18 Commits

Author SHA1 Message Date
Burak Kaan Köse
bc4838578e Handling null client connection while sending server response. 2024-08-13 22:57:36 +02:00
Burak Kaan Köse
548996405a Fix incorrect accounts' mails are going to different accounts. 2024-08-13 22:54:36 +02:00
Burak Kaan Köse
a9a5f0bd14 Ascending downloading of mails since some servers require it. 2024-08-13 22:54:14 +02:00
Burak Kaan Köse
ec05ff6123 Optional splash screen. 2024-08-13 19:26:24 +02:00
Burak Kaan Köse
10c7ab421b Setting exception on connection failure. 2024-08-13 16:14:25 +02:00
Burak Kaan Köse
a8a5cc53ea Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-08-13 16:13:25 +02:00
Burak Kaan Köse
8fe48ca438 Fixed an issue where reconnecting doesn't await the handle in the second attempt. 2024-08-13 16:12:34 +02:00
Tiktack
cbd5a515a9 Fix account signature preferences during draft creation (#314)
* Pass account ID instead of account to draft creation method, since account object can be stale.

* Configure await
2024-08-12 00:56:26 +02:00
Tiktack
5912adff93 Embedded images replaced with cid linked resources. (#313)
* Added logic to replace embedded images with linked resources

* Added alt text for images and replaced NewtonSoft with Text.Json

* Fix draft mime preparation

* Fix crashes for signatures without images.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2024-08-11 23:58:54 +02:00
Burak Kaan Köse
983bc21448 Removing server init from the app init. Making sure server connection is established before doing a request. Handling Connecting state. 2024-08-11 15:25:40 +02:00
Burak Kaan Köse
6d08368462 Hiding reconnect flyout on clicking reconnect. 2024-08-11 15:18:23 +02:00
Burak Kaan Köse
cde7bb3524 Merged main. 2024-08-10 14:35:26 +02:00
Burak Kaan Köse
133dc91561 Prevent crashes on invalid Uri for protocol activation. 2024-08-10 14:35:01 +02:00
Tiktack
f408f59beb Improve mailto links handling (#310)
* Refactor draft creation

* try scoped namespace

* Refactor mailto protocol and revert namespaces

* Remove useless account query

* Fix typo and CC/BCC in replies

* Replace convert with existing extension

* Small fixes

* Fix CC/Bcc in replies to automatically show if needed.

* Fixed body parameter position from mailto parameters

* Fixed issue with ReplyAll self not removed
2024-08-10 14:33:02 +02:00
Burak Kaan Köse
8763bf11ab Fix typo. 2024-08-09 14:23:51 +02:00
Burak Kaan Köse
99592a52be Unregistration condition. 2024-08-09 02:02:11 +02:00
Burak Kaan Köse
25a8a52573 Remove SessionConnectedTask 2024-08-09 01:51:21 +02:00
Burak Kaan Köse
5901344459 Remove SessionConntectedTask. 2024-08-09 01:24:55 +02:00
53 changed files with 657 additions and 804 deletions

View File

@@ -1,19 +0,0 @@
using System;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Background;
namespace Wino.BackgroundTasks
{
public sealed class SessionConnectedTask : IBackgroundTask
{
public async void Run(IBackgroundTaskInstance taskInstance)
{
var def = taskInstance.GetDeferral();
// Run server on session connected by launching the Full Thrust process.
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
def.Complete();
}
}
}

View File

@@ -104,7 +104,6 @@
<ItemGroup> <ItemGroup>
<Compile Include="AppUpdatedTask.cs" /> <Compile Include="AppUpdatedTask.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SessionConnectedTask.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform"> <PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">

View File

@@ -1,16 +1,7 @@
using System.Threading.Tasks; namespace Wino.Core.Domain.Interfaces
namespace Wino.Core.Domain.Interfaces
{ {
public interface IBackgroundTaskService public interface IBackgroundTaskService
{ {
/// <summary>
/// Manages background task registrations, requests access if needed, checks the statusses of them etc.
/// </summary>
/// <exception cref="BackgroundTaskExecutionRequestDeniedException">If the access request is denied for some reason.</exception>
/// <exception cref="BackgroundTaskRegistrationFailedException">If one of the requires background tasks are failed during registration.</exception>
Task HandleBackgroundTaskRegistrations();
/// <summary> /// <summary>
/// Unregisters all existing background tasks. Useful for migrations. /// Unregisters all existing background tasks. Useful for migrations.
/// </summary> /// </summary>

View File

@@ -1,10 +1,16 @@
using System.Collections.Specialized; using Wino.Core.Domain.Models.Launch;
namespace Wino.Core.Domain.Interfaces namespace Wino.Core.Domain.Interfaces;
public interface ILaunchProtocolService
{ {
public interface ILaunchProtocolService /// <summary>
{ /// Used to handle toasts.
object LaunchParameter { get; set; } /// </summary>
NameValueCollection MailtoParameters { get; set; } object LaunchParameter { get; set; }
}
/// <summary>
/// Used to handle mailto links.
/// </summary>
MailToUri MailToUri { get; set; }
} }

View File

@@ -11,7 +11,6 @@ namespace Wino.Core.Domain.Interfaces
{ {
Task<MailCopy> GetSingleMailItemAsync(string mailCopyId, string remoteFolderId); Task<MailCopy> GetSingleMailItemAsync(string mailCopyId, string remoteFolderId);
Task<MailCopy> GetSingleMailItemAsync(Guid uniqueMailId); Task<MailCopy> GetSingleMailItemAsync(Guid uniqueMailId);
Task<MailCopy> CreateDraftAsync(MailAccount composerAccount, string generatedReplyMimeMessageBase64, MimeMessage replyingMimeMessage = null, IMailItem replyingMailItem = null);
Task<List<IMailItem>> FetchMailsAsync(MailListInitializationOptions options); Task<List<IMailItem>> FetchMailsAsync(MailListInitializationOptions options);
/// <summary> /// <summary>
@@ -44,23 +43,12 @@ namespace Wino.Core.Domain.Interfaces
/// <summary> /// <summary>
/// Maps new mail item with the existing local draft copy. /// Maps new mail item with the existing local draft copy.
///
/// </summary> /// </summary>
/// <param name="newMailCopyId"></param> /// <param name="newMailCopyId"></param>
/// <param name="newDraftId"></param> /// <param name="newDraftId"></param>
/// <param name="newThreadId"></param> /// <param name="newThreadId"></param>
Task MapLocalDraftAsync(string newMailCopyId, string newDraftId, string newThreadId); Task MapLocalDraftAsync(string newMailCopyId, string newDraftId, string newThreadId);
/// <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); Task UpdateMailAsync(MailCopy mailCopy);
/// <summary> /// <summary>
@@ -106,9 +94,18 @@ namespace Wino.Core.Domain.Interfaces
/// Checks whether the mail exists in the folder. /// 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. /// When deciding Create or Update existing mail, we need to check if the mail exists in the folder.
/// </summary> /// </summary>
/// <param name="messageId">Message id</param> /// <param name="mailCopyId">MailCopy id</param>
/// <param name="folderId">Folder's local id.</param> /// <param name="folderId">Folder's local id.</param>
/// <returns>Whether mail exists in the folder or not.</returns> /// <returns>Whether mail exists in the folder or not.</returns>
Task<bool> IsMailExistsAsync(string mailCopyId, Guid folderId); Task<bool> IsMailExistsAsync(string mailCopyId, Guid folderId);
/// <summary>
/// Creates a draft MailCopy and MimeMessage based on the given options.
/// For forward/reply it would include the referenced message.
/// </summary>
/// <param name="accountId">AccountId which should have new draft.</param>
/// <param name="draftCreationOptions">Options like new email/forward/draft.</param>
/// <returns>Draft MailCopy and Draft MimeMessage as base64.</returns>
Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions);
} }
} }

View File

@@ -16,7 +16,7 @@ namespace Wino.Core.Domain.Interfaces
/// Queues new draft creation request for synchronizer. /// Queues new draft creation request for synchronizer.
/// </summary> /// </summary>
/// <param name="draftPreperationRequest">A class that holds the parameters for creating a draft.</param> /// <param name="draftPreperationRequest">A class that holds the parameters for creating a draft.</param>
Task ExecuteAsync(DraftPreperationRequest draftPreperationRequest); Task ExecuteAsync(DraftPreparationRequest draftPreperationRequest);
/// <summary> /// <summary>
/// Queues a new request for synchronizer to send a draft. /// Queues a new request for synchronizer to send a draft.

View File

@@ -19,19 +19,13 @@ namespace Wino.Core.Domain.Interfaces
/// <summary> /// <summary>
/// Launches Full Trust process (Wino Server) and awaits connection completion. /// Launches Full Trust process (Wino Server) and awaits connection completion.
/// If connection is not established in 5 seconds, it will return false. /// If connection is not established in 10 seconds, it will return false.
/// If the server process is already running, it'll connect to existing one. /// 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. /// If the server process is not running, it'll be launched and connection establishment is awaited.
/// </summary> /// </summary>
/// <returns>Whether connection is established or not.</returns> /// <returns>Whether connection is established or not.</returns>
Task<bool> ConnectAsync(); 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> /// <summary>
/// Queues a new user request to be processed by Wino Server. /// Queues a new user request to be processed by Wino Server.
/// Healthy connection must present before calling this method. /// Healthy connection must present before calling this method.
@@ -48,6 +42,13 @@ namespace Wino.Core.Domain.Interfaces
/// <param name="clientMessage">Request type.</param> /// <param name="clientMessage">Request type.</param>
/// <returns>Response received from the server for the given TResponse type.</returns> /// <returns>Response received from the server for the given TResponse type.</returns>
Task<WinoServerResponse<TResponse>> GetResponseAsync<TResponse, TRequestType>(TRequestType clientMessage) where TRequestType : IClientMessage; Task<WinoServerResponse<TResponse>> GetResponseAsync<TResponse, TRequestType>(TRequestType clientMessage) where TRequestType : IClientMessage;
/// <summary>
/// Handle for connecting to the server.
/// If the server is already running, it'll connect to existing one.
/// Callers can await this handle to wait for connection establishment.
/// </summary>
TaskCompletionSource<bool> ConnectingHandle { get; }
} }
public interface IWinoServerConnectionManager<TAppServiceConnection> : IWinoServerConnectionManager, IInitializeAsync public interface IWinoServerConnectionManager<TAppServiceConnection> : IWinoServerConnectionManager, IInitializeAsync

View File

@@ -1,22 +1,22 @@
using Newtonsoft.Json; using System.Text.Json.Serialization;
namespace Wino.Core.Domain.Models.AutoDiscovery namespace Wino.Core.Domain.Models.AutoDiscovery
{ {
public class AutoDiscoveryProviderSetting public class AutoDiscoveryProviderSetting
{ {
[JsonProperty("protocol")] [JsonPropertyName("protocol")]
public string Protocol { get; set; } public string Protocol { get; set; }
[JsonProperty("address")] [JsonPropertyName("address")]
public string Address { get; set; } public string Address { get; set; }
[JsonProperty("port")] [JsonPropertyName("port")]
public int Port { get; set; } public int Port { get; set; }
[JsonProperty("secure")] [JsonPropertyName("secure")]
public string Secure { get; set; } public string Secure { get; set; }
[JsonProperty("username")] [JsonPropertyName("username")]
public string Username { get; set; } public string Username { get; set; }
} }
} }

View File

@@ -1,19 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using System.Text.Json.Serialization;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
namespace Wino.Core.Domain.Models.AutoDiscovery namespace Wino.Core.Domain.Models.AutoDiscovery
{ {
public class AutoDiscoverySettings public class AutoDiscoverySettings
{ {
[JsonProperty("domain")] [JsonPropertyName("domain")]
public string Domain { get; set; } public string Domain { get; set; }
[JsonProperty("password")] [JsonPropertyName("password")]
public string Password { get; set; } public string Password { get; set; }
[JsonProperty("settings")] [JsonPropertyName("settings")]
public List<AutoDiscoveryProviderSetting> Settings { get; set; } public List<AutoDiscoveryProviderSetting> Settings { get; set; }
/// <summary> /// <summary>

View File

@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Wino.Core.Domain.Models.Launch;
public class MailToUri
{
public string Subject { get; private set; }
public string Body { get; private set; }
public List<string> To { get; } = [];
public List<string> Cc { get; } = [];
public List<string> Bcc { get; } = [];
public Dictionary<string, string> OtherParameters { get; } = [];
public MailToUri(string mailToUrl)
{
ParseMailToUrl(mailToUrl);
}
private void ParseMailToUrl(string mailToUrl)
{
if (string.IsNullOrWhiteSpace(mailToUrl))
throw new ArgumentException("mailtoUrl cannot be null or empty.", nameof(mailToUrl));
if (!mailToUrl.StartsWith("mailto:", StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("URL must start with 'mailto:'.", nameof(mailToUrl));
var mailToWithoutScheme = mailToUrl.Substring(7); // Remove "mailto:"
var components = mailToWithoutScheme.Split('?');
if (!string.IsNullOrEmpty(components[0]))
{
To.AddRange(components[0].Split(',').Select(email => HttpUtility.UrlDecode(email).Trim()));
}
if (components.Length <= 1)
{
return;
}
var parameters = components[1].Split('&');
foreach (var parameter in parameters)
{
var keyValue = parameter.Split('=');
if (keyValue.Length != 2)
continue;
var key = keyValue[0].ToLowerInvariant();
var value = HttpUtility.UrlDecode(keyValue[1]);
switch (key)
{
case "to":
To.AddRange(value.Split(',').Select(email => email.Trim()));
break;
case "subject":
Subject = value;
break;
case "body":
Body = value;
break;
case "cc":
Cc.AddRange(value.Split(',').Select(email => email.Trim()));
break;
case "bcc":
Bcc.AddRange(value.Split(',').Select(email => email.Trim()));
break;
default:
OtherParameters[key] = value;
break;
}
}
}
}

View File

@@ -1,42 +1,27 @@
using System.Collections.Specialized; using MimeKit;
using System.Linq;
using System.Text.Json.Serialization;
using MimeKit;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Launch;
namespace Wino.Core.Domain.Models.MailItem namespace Wino.Core.Domain.Models.MailItem;
public class DraftCreationOptions
{ {
public class DraftCreationOptions public DraftCreationReason Reason { get; set; }
{
[JsonIgnore]
public MimeMessage ReferenceMimeMessage { get; set; }
public MailCopy ReferenceMailCopy { get; set; }
public DraftCreationReason Reason { get; set; }
#region Mailto Protocol Related Stuff /// <summary>
/// Used for forward/reply
/// </summary>
public ReferencedMessage ReferencedMessage { get; set; }
public const string MailtoSubjectParameterKey = "subject"; /// <summary>
public const string MailtoBodyParameterKey = "body"; /// Used to create mails from Mailto links
public const string MailtoToParameterKey = "mailto"; /// </summary>
public const string MailtoCCParameterKey = "cc"; public MailToUri MailToUri { get; set; }
public const string MailtoBCCParameterKey = "bcc"; }
public NameValueCollection MailtoParameters { get; set; } public class ReferencedMessage
{
private bool IsMailtoParameterExists(string parameterKey) public MailCopy MailCopy { get; set; }
=> MailtoParameters != null public MimeMessage MimeMessage { get; set; }
&& MailtoParameters.AllKeys.Contains(parameterKey);
public bool TryGetMailtoValue(string key, out string value)
{
bool valueExists = IsMailtoParameterExists(key);
value = valueExists ? MailtoParameters[key] : string.Empty;
return valueExists;
}
#endregion
}
} }

View File

@@ -0,0 +1,48 @@
using System;
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 DraftPreparationRequest
{
public DraftPreparationRequest(MailAccount account, MailCopy createdLocalDraftCopy, string base64EncodedMimeMessage, MailCopy referenceMailCopy = null)
{
Account = account ?? throw new ArgumentNullException(nameof(account));
CreatedLocalDraftCopy = createdLocalDraftCopy ?? throw new ArgumentNullException(nameof(createdLocalDraftCopy));
ReferenceMailCopy = referenceMailCopy;
// 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 DraftPreparationRequest() { }
public MailCopy CreatedLocalDraftCopy { get; set; }
public MailCopy ReferenceMailCopy { get; set; }
public string Base64LocalDraftMimeMessage { get; set; }
[JsonIgnore]
private MimeMessage createdLocalDraftMimeMessage;
[JsonIgnore]
public MimeMessage CreatedLocalDraftMimeMessage
{
get
{
createdLocalDraftMimeMessage ??= Base64LocalDraftMimeMessage.GetMimeMessageFromBase64();
return createdLocalDraftMimeMessage;
}
}
public MailAccount Account { get; }
}

View File

@@ -1,49 +0,0 @@
using System;
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, string base64EncodedMimeMessage)
{
Account = account ?? throw new ArgumentNullException(nameof(account));
CreatedLocalDraftCopy = createdLocalDraftCopy ?? throw new ArgumentNullException(nameof(createdLocalDraftCopy));
// 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 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; }
}
}

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace Wino.Core.Domain.Models.Reader;
public class ImageInfo
{
[JsonPropertyName("data")]
public string Data { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}

View File

@@ -1,4 +1,4 @@
using Newtonsoft.Json; using System.Text.Json.Serialization;
namespace Wino.Core.Domain.Models.Reader namespace Wino.Core.Domain.Models.Reader
{ {
@@ -7,10 +7,10 @@ namespace Wino.Core.Domain.Models.Reader
/// </summary> /// </summary>
public class WebViewMessage public class WebViewMessage
{ {
[JsonProperty("type")] [JsonPropertyName("type")]
public string Type { get; set; } public string Type { get; set; }
[JsonProperty("value")] [JsonPropertyName("value")]
public string Value { get; set; } public string Value { get; set; }
} }
} }

View File

@@ -13,8 +13,6 @@ namespace Wino.Core.Domain.Models.Server
public string Message { get; set; } public string Message { get; set; }
public T Data { get; set; } public T Data { get; set; }
// protected WinoServerResponse() { }
public static WinoServerResponse<T> CreateSuccessResponse(T data) public static WinoServerResponse<T> CreateSuccessResponse(T data)
{ {
return new WinoServerResponse<T> return new WinoServerResponse<T>

View File

@@ -62,7 +62,6 @@
</PackageReference> </PackageReference>
<PackageReference Include="MimeKit" Version="4.7.1" /> <PackageReference Include="MimeKit" Version="4.7.1" />
<PackageReference Include="MailKit" Version="4.7.1.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="sqlite-net-pcl" Version="1.8.116" />
<PackageReference Include="System.Text.Json" Version="8.0.4" /> <PackageReference Include="System.Text.Json" Version="8.0.4" />
</ItemGroup> </ItemGroup>

View File

@@ -1,6 +1,4 @@
using System; using Serilog;
using System.Threading.Tasks;
using Serilog;
using Windows.ApplicationModel.Background; using Windows.ApplicationModel.Background;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
@@ -8,12 +6,7 @@ namespace Wino.Core.UWP.Services
{ {
public class BackgroundTaskService : IBackgroundTaskService public class BackgroundTaskService : IBackgroundTaskService
{ {
private const string Is180BackgroundTasksRegisteredKey = nameof(Is180BackgroundTasksRegisteredKey); private const string IsBackgroundTasksUnregisteredKey = nameof(IsBackgroundTasksUnregisteredKey);
public const string ToastActivationTaskEx = nameof(ToastActivationTaskEx);
private const string SessionConnectedTaskEntryPoint = "Wino.BackgroundTasks.SessionConnectedTask";
private const string SessionConnectedTaskName = "SessionConnectedTask";
private readonly IConfigurationService _configurationService; private readonly IConfigurationService _configurationService;
@@ -22,48 +15,18 @@ namespace Wino.Core.UWP.Services
_configurationService = configurationService; _configurationService = configurationService;
} }
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)
{
// Unregister all tasks and register new ones.
UnregisterAllBackgroundTask();
RegisterSessionConnectedTask();
_configurationService.Set(Is180BackgroundTasksRegisteredKey, true);
}
}
public void UnregisterAllBackgroundTask() public void UnregisterAllBackgroundTask()
{ {
foreach (var task in BackgroundTaskRegistration.AllTasks) if (!_configurationService.Get(IsBackgroundTasksUnregisteredKey, false))
{ {
task.Value.Unregister(true); foreach (var task in BackgroundTaskRegistration.AllTasks)
{
task.Value.Unregister(true);
}
Log.Information("Unregistered all background tasks.");
_configurationService.Set(IsBackgroundTasksUnregisteredKey, true);
} }
Log.Information("Unregistered all background tasks.");
}
private BackgroundTaskRegistration RegisterSessionConnectedTask()
{
var builder = new BackgroundTaskBuilder
{
Name = SessionConnectedTaskName,
TaskEntryPoint = SessionConnectedTaskEntryPoint
};
builder.SetTrigger(new SystemTrigger(SystemTriggerType.SessionConnected, false));
return builder.Register();
} }
} }
} }

View File

@@ -4,10 +4,10 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime; using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Toolkit.Uwp.Helpers; using Microsoft.Toolkit.Uwp.Helpers;
using Newtonsoft.Json;
using Windows.Storage; using Windows.Storage;
using Windows.UI; using Windows.UI;
using Windows.UI.ViewManagement; using Windows.UI.ViewManagement;
@@ -406,7 +406,7 @@ namespace Wino.Services
// Save metadata. // Save metadata.
var metadataFile = await themeFolder.CreateFileAsync($"{newTheme.Id}.json", CreationCollisionOption.ReplaceExisting); var metadataFile = await themeFolder.CreateFileAsync($"{newTheme.Id}.json", CreationCollisionOption.ReplaceExisting);
var serialized = JsonConvert.SerializeObject(newTheme); var serialized = JsonSerializer.Serialize(newTheme);
await FileIO.WriteTextAsync(metadataFile, serialized); await FileIO.WriteTextAsync(metadataFile, serialized);
return newTheme; return newTheme;
@@ -438,7 +438,7 @@ namespace Wino.Services
{ {
var fileContent = await FileIO.ReadTextAsync(file); var fileContent = await FileIO.ReadTextAsync(file);
return JsonConvert.DeserializeObject<CustomThemeMetadata>(fileContent); return JsonSerializer.Deserialize<CustomThemeMetadata>(fileContent);
} }
public string GetSystemAccentColorHex() public string GetSystemAccentColorHex()

View File

@@ -24,12 +24,13 @@ namespace Wino.Core.UWP.Services
{ {
public class WinoServerConnectionManager : public class WinoServerConnectionManager :
IWinoServerConnectionManager<AppServiceConnection>, IWinoServerConnectionManager<AppServiceConnection>,
IRecipient<WinoServerConnectionEstrablished> IRecipient<WinoServerConnectionEstablished>
{ {
private const int ServerConnectionTimeoutMs = 5000; private const int ServerConnectionTimeoutMs = 10000;
public event EventHandler<WinoServerConnectionStatus> StatusChanged; public event EventHandler<WinoServerConnectionStatus> StatusChanged;
private TaskCompletionSource<bool> _connectionTaskCompletionSource;
public TaskCompletionSource<bool> ConnectingHandle { get; private set; }
private ILogger Logger => Logger.ForContext<WinoServerConnectionManager>(); private ILogger Logger => Logger.ForContext<WinoServerConnectionManager>();
@@ -40,6 +41,7 @@ namespace Wino.Core.UWP.Services
get { return status; } get { return status; }
private set private set
{ {
Log.Information("Server connection status changed to {Status}.", value);
status = value; status = value;
StatusChanged?.Invoke(this, value); StatusChanged?.Invoke(this, value);
} }
@@ -85,52 +87,85 @@ namespace Wino.Core.UWP.Services
public async Task<bool> ConnectAsync() public async Task<bool> ConnectAsync()
{ {
if (Status == WinoServerConnectionStatus.Connected) return true; if (Status == WinoServerConnectionStatus.Connected)
{
Log.Information("Server is already connected.");
return true;
}
if (Status == WinoServerConnectionStatus.Connecting)
{
// A connection is already being established at the moment.
// No need to run another connection establishment process.
// Await the connecting handler if possible.
if (ConnectingHandle != null)
{
return await ConnectingHandle.Task;
}
}
if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0)) if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0))
{ {
try try
{ {
_connectionTaskCompletionSource ??= new TaskCompletionSource<bool>(); ConnectingHandle = new TaskCompletionSource<bool>();
var connectionCancellationToken = new CancellationTokenSource(TimeSpan.FromMilliseconds(ServerConnectionTimeoutMs));
Status = WinoServerConnectionStatus.Connecting; Status = WinoServerConnectionStatus.Connecting;
var connectionCancellationToken = new CancellationTokenSource(TimeSpan.FromMilliseconds(ServerConnectionTimeoutMs));
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync(); await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
// Connection establishment handler is in App.xaml.cs OnBackgroundActivated. // Connection establishment handler is in App.xaml.cs OnBackgroundActivated.
// Once the connection is established, the handler will set the Connection property // Once the connection is established, the handler will set the Connection property
// and WinoServerConnectionEstrablished will be fired by the messenger. // and WinoServerConnectionEstablished will be fired by the messenger.
await _connectionTaskCompletionSource.Task.WaitAsync(connectionCancellationToken.Token); await ConnectingHandle.Task.WaitAsync(connectionCancellationToken.Token);
Log.Information("Server connection established successfully.");
} }
catch (Exception) catch (OperationCanceledException canceledException)
{ {
Log.Error(canceledException, $"Server process did not start in {ServerConnectionTimeoutMs} ms. Operation is canceled.");
ConnectingHandle?.TrySetException(canceledException);
Status = WinoServerConnectionStatus.Failed;
return false;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to connect to the server.");
ConnectingHandle?.TrySetException(ex);
Status = WinoServerConnectionStatus.Failed; Status = WinoServerConnectionStatus.Failed;
return false; return false;
} }
return true; return true;
} }
else
{
Log.Information("FullTrustAppContract is not present in the system. Server connection is not possible.");
}
return false; 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() public async Task InitializeAsync()
{ {
var isConnectionSuccessfull = await ConnectAsync(); var isConnectionSuccessfull = await ConnectAsync();
// TODO: Log connection status if (isConnectionSuccessfull)
{
Log.Information("ServerConnectionManager initialized successfully.");
}
else
{
Log.Error("ServerConnectionManager initialization failed.");
}
} }
private void ServerMessageReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args) private void ServerMessageReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
@@ -222,7 +257,7 @@ namespace Wino.Core.UWP.Services
private void ServerDisconnected(AppServiceConnection sender, AppServiceClosedEventArgs args) private void ServerDisconnected(AppServiceConnection sender, AppServiceClosedEventArgs args)
{ {
// TODO: Handle server disconnection. Log.Information("Server disconnected.");
} }
public async Task QueueRequestAsync(IRequestBase request, Guid accountId) public async Task QueueRequestAsync(IRequestBase request, Guid accountId)
@@ -242,8 +277,8 @@ namespace Wino.Core.UWP.Services
private async Task<WinoServerResponse<TResponse>> GetResponseInternalAsync<TResponse, TRequestType>(TRequestType message, Dictionary<string, object> parameters = null) private async Task<WinoServerResponse<TResponse>> GetResponseInternalAsync<TResponse, TRequestType>(TRequestType message, Dictionary<string, object> parameters = null)
{ {
if (Connection == null) if (Status != WinoServerConnectionStatus.Connected)
return WinoServerResponse<TResponse>.CreateErrorResponse("Server connection is not established."); await ConnectAsync();
string serializedMessage = string.Empty; string serializedMessage = string.Empty;
@@ -305,12 +340,7 @@ namespace Wino.Core.UWP.Services
} }
} }
public void Receive(WinoServerConnectionEstrablished message) public void Receive(WinoServerConnectionEstablished message)
{ => ConnectingHandle?.TrySetResult(true);
if (_connectionTaskCompletionSource != null)
{
_connectionTaskCompletionSource.TrySetResult(true);
}
}
} }
} }

View File

@@ -1,8 +1,8 @@
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Text.Json.Nodes;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
@@ -58,14 +58,14 @@ namespace Wino.Core.Authenticators
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
throw new GoogleAuthenticationException(Translator.Exception_GoogleAuthorizationCodeExchangeFailed); throw new GoogleAuthenticationException(Translator.Exception_GoogleAuthorizationCodeExchangeFailed);
var parsed = JObject.Parse(responseString); var parsed = JsonNode.Parse(responseString).AsObject();
if (parsed.ContainsKey("error")) if (parsed.ContainsKey("error"))
throw new GoogleAuthenticationException(parsed["error"]["message"].Value<string>()); throw new GoogleAuthenticationException(parsed["error"]["message"].GetValue<string>());
var accessToken = parsed["access_token"].Value<string>(); var accessToken = parsed["access_token"].GetValue<string>();
var refreshToken = parsed["refresh_token"].Value<string>(); var refreshToken = parsed["refresh_token"].GetValue<string>();
var expiresIn = parsed["expires_in"].Value<long>(); var expiresIn = parsed["expires_in"].GetValue<long>();
var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn); var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn);
@@ -76,12 +76,12 @@ namespace Wino.Core.Authenticators
var userinfoResponse = await client.GetAsync(UserInfoEndpoint); var userinfoResponse = await client.GetAsync(UserInfoEndpoint);
string userinfoResponseContent = await userinfoResponse.Content.ReadAsStringAsync(); string userinfoResponseContent = await userinfoResponse.Content.ReadAsStringAsync();
var parsedUserInfo = JObject.Parse(userinfoResponseContent); var parsedUserInfo = JsonNode.Parse(userinfoResponseContent).AsObject();
if (parsedUserInfo.ContainsKey("error")) if (parsedUserInfo.ContainsKey("error"))
throw new GoogleAuthenticationException(parsedUserInfo["error"]["message"].Value<string>()); throw new GoogleAuthenticationException(parsedUserInfo["error"]["message"].GetValue<string>());
var username = parsedUserInfo["emailAddress"].Value<string>(); var username = parsedUserInfo["emailAddress"].GetValue<string>();
return new TokenInformation() return new TokenInformation()
{ {
@@ -166,13 +166,13 @@ namespace Wino.Core.Authenticators
string responseString = await response.Content.ReadAsStringAsync(); string responseString = await response.Content.ReadAsStringAsync();
var parsed = JObject.Parse(responseString); var parsed = JsonNode.Parse(responseString).AsObject();
// TODO: Error parsing is incorrect. // TODO: Error parsing is incorrect.
if (parsed.ContainsKey("error")) if (parsed.ContainsKey("error"))
throw new GoogleAuthenticationException(parsed["error_description"].Value<string>()); throw new GoogleAuthenticationException(parsed["error_description"].GetValue<string>());
var accessToken = parsed["access_token"].Value<string>(); var accessToken = parsed["access_token"].GetValue<string>();
string activeRefreshToken = refresh_token; string activeRefreshToken = refresh_token;
@@ -182,10 +182,10 @@ namespace Wino.Core.Authenticators
if (parsed.ContainsKey("refresh_token")) if (parsed.ContainsKey("refresh_token"))
{ {
activeRefreshToken = parsed["refresh_token"].Value<string>(); activeRefreshToken = parsed["refresh_token"].GetValue<string>();
} }
var expiresIn = parsed["expires_in"].Value<long>(); var expiresIn = parsed["expires_in"].GetValue<long>();
var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn); var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn);
return new TokenInformationBase() return new TokenInformationBase()

View File

@@ -1,9 +1,12 @@
using System.IO; using System;
using System.IO;
using System.Text; using System.Text;
using Google.Apis.Gmail.v1.Data; using Google.Apis.Gmail.v1.Data;
using HtmlAgilityPack;
using MimeKit; using MimeKit;
using MimeKit.IO; using MimeKit.IO;
using MimeKit.IO.Filters; using MimeKit.IO.Filters;
using MimeKit.Utils;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
@@ -48,5 +51,71 @@ namespace Wino.Core.Extensions
return new AddressInformation() { Name = address.Name, Address = address.Address }; return new AddressInformation() { Name = address.Name, Address = address.Address };
} }
/// <summary>
/// Sets html body replacing base64 images with cid linked resources.
/// Updates text body based on html.
/// </summary>
/// <param name="bodyBuilder">Body builder.</param>
/// <param name="htmlContent">Html content that can have embedded images.</param>
/// <returns>Body builder with set HtmlBody.</returns>
public static BodyBuilder SetHtmlBody(this BodyBuilder bodyBuilder, string htmlContent)
{
if (string.IsNullOrEmpty(htmlContent)) return bodyBuilder;
var doc = new HtmlDocument();
doc.LoadHtml(htmlContent);
var imgNodes = doc.DocumentNode.SelectNodes("//img");
if (imgNodes != null)
{
foreach (var node in imgNodes)
{
var src = node.GetAttributeValue("src", string.Empty);
if (string.IsNullOrEmpty(src)) continue;
if (!src.StartsWith("data:image"))
{
continue;
}
var parts = src.Substring(11).Split([";base64,"], StringSplitOptions.None);
string mimeType = parts[0];
string base64Content = parts[1];
var alt = node.GetAttributeValue("alt", $"Embedded_Image.{mimeType}");
// Convert the base64 content to binary data
byte[] imageData = Convert.FromBase64String(base64Content);
// Create a new linked resource as MimePart
var image = new MimePart("image", mimeType)
{
ContentId = MimeUtils.GenerateMessageId(),
Content = new MimeContent(new MemoryStream(imageData)),
ContentDisposition = new ContentDisposition(ContentDisposition.Inline),
ContentDescription = alt.Replace(" ", "_"),
FileName = alt,
ContentTransferEncoding = ContentEncoding.Base64
};
bodyBuilder.LinkedResources.Add(image);
node.SetAttributeValue("src", $"cid:{image.ContentId}");
}
}
bodyBuilder.HtmlBody = doc.DocumentNode.InnerHtml;
if (!string.IsNullOrEmpty(bodyBuilder.HtmlBody))
{
bodyBuilder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(bodyBuilder.HtmlBody);
}
return bodyBuilder;
}
} }
} }

View File

@@ -1,36 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace Wino.Core.Http
{
/// <summary>
/// We need to generate HttpRequestMessage for batch requests, and sometimes we need to
/// serialize content as json. However, some of the fields like 'ODataType' must be ignored
/// in order PATCH requests to succeed. Therefore Microsoft account synchronizer uses
/// special JsonSerializerSettings for ignoring some of the properties.
/// </summary>
public class MicrosoftJsonContractResolver : DefaultContractResolver
{
private readonly HashSet<string> ignoreProps = new HashSet<string>()
{
"ODataType"
};
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
if (ignoreProps.Contains(property.PropertyName))
{
property.ShouldSerialize = _ => false;
}
return property;
}
}
}

View File

@@ -1,15 +1,13 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Models.Requests namespace Wino.Core.Domain.Models.Requests
{ {
/// <summary> /// <summary>
/// Bundle that encapsulates batch request and native request without a response. /// Bundle that encapsulates batch request and native request without a response.
/// </summary> /// </summary>
@@ -43,7 +41,7 @@ namespace Wino.Core.Domain.Models.Requests
{ {
var content = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); var content = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
return JsonConvert.DeserializeObject<TResponse>(content) ?? throw new InvalidOperationException("Invalid Http Response Deserialization"); return JsonSerializer.Deserialize<TResponse>(content) ?? throw new InvalidOperationException("Invalid Http Response Deserialization");
} }
public override string ToString() public override string ToString()

View File

@@ -11,7 +11,7 @@ using Wino.Messaging.UI;
namespace Wino.Core.Requests namespace Wino.Core.Requests
{ {
public record CreateDraftRequest(DraftPreperationRequest DraftPreperationRequest) public record CreateDraftRequest(DraftPreparationRequest DraftPreperationRequest)
: RequestBase<BatchCreateDraftRequest>(DraftPreperationRequest.CreatedLocalDraftCopy, MailSynchronizerOperation.CreateDraft), : RequestBase<BatchCreateDraftRequest>(DraftPreperationRequest.CreatedLocalDraftCopy, MailSynchronizerOperation.CreateDraft),
ICustomFolderSynchronizationRequest ICustomFolderSynchronizationRequest
{ {
@@ -36,7 +36,7 @@ namespace Wino.Core.Requests
} }
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
public record class BatchCreateDraftRequest(IEnumerable<IRequest> Items, DraftPreperationRequest DraftPreperationRequest) public record class BatchCreateDraftRequest(IEnumerable<IRequest> Items, DraftPreparationRequest DraftPreperationRequest)
: BatchRequestBase(Items, MailSynchronizerOperation.CreateDraft) : BatchRequestBase(Items, MailSynchronizerOperation.CreateDraft)
{ {
public override void ApplyUIChanges() public override void ApplyUIChanges()

View File

@@ -1,7 +1,7 @@
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json;
using Serilog; using Serilog;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.AutoDiscovery; using Wino.Core.Domain.Models.AutoDiscovery;
@@ -43,7 +43,7 @@ namespace Wino.Core.Services
{ {
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<AutoDiscoverySettings>(content); return JsonSerializer.Deserialize<AutoDiscoverySettings>(content);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -0,0 +1,10 @@
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Launch;
namespace Wino.Core.Services;
public class LaunchProtocolService : ILaunchProtocolService
{
public object LaunchParameter { get; set; }
public MailToUri MailToUri { get; set; }
}

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions.Extensions; using Microsoft.Kiota.Abstractions.Extensions;
@@ -32,7 +31,6 @@ namespace Wino.Core.Services
private readonly IMimeFileService _mimeFileService; private readonly IMimeFileService _mimeFileService;
private readonly IPreferencesService _preferencesService; private readonly IPreferencesService _preferencesService;
private readonly ILogger _logger = Log.ForContext<MailService>(); private readonly ILogger _logger = Log.ForContext<MailService>();
public MailService(IDatabaseService databaseService, public MailService(IDatabaseService databaseService,
@@ -53,18 +51,10 @@ namespace Wino.Core.Services
_preferencesService = preferencesService; _preferencesService = preferencesService;
} }
public async Task<MailCopy> CreateDraftAsync(MailAccount composerAccount, public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions)
string generatedReplyMimeMessageBase64,
MimeMessage replyingMimeMessage = null,
IMailItem replyingMailItem = null)
{ {
var createdDraftMimeMessage = generatedReplyMimeMessageBase64.GetMimeMessageFromBase64(); var composerAccount = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
var createdDraftMimeMessage = await CreateDraftMimeAsync(composerAccount, draftCreationOptions);
bool isImapAccount = composerAccount.ServerInformation != null;
string fromName;
fromName = composerAccount.SenderName;
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(composerAccount.Id, SpecialFolderType.Draft); var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(composerAccount.Id, SpecialFolderType.Draft);
@@ -78,7 +68,7 @@ namespace Wino.Core.Services
Id = Guid.NewGuid().ToString(), // This will be replaced after network call with the remote draft id. Id = Guid.NewGuid().ToString(), // This will be replaced after network call with the remote draft id.
CreationDate = DateTime.UtcNow, CreationDate = DateTime.UtcNow,
FromAddress = composerAccount.Address, FromAddress = composerAccount.Address,
FromName = fromName, FromName = composerAccount.SenderName,
HasAttachments = false, HasAttachments = false,
Importance = MailImportance.Normal, Importance = MailImportance.Normal,
Subject = createdDraftMimeMessage.Subject, Subject = createdDraftMimeMessage.Subject,
@@ -93,28 +83,25 @@ namespace Wino.Core.Services
}; };
// If replying, add In-Reply-To, ThreadId and References. // If replying, add In-Reply-To, ThreadId and References.
bool isReplying = replyingMimeMessage != null; if (draftCreationOptions.ReferencedMessage != null)
if (isReplying)
{ {
if (replyingMimeMessage.References != null) if (draftCreationOptions.ReferencedMessage.MimeMessage.References != null)
copy.References = string.Join(",", replyingMimeMessage.References); copy.References = string.Join(",", draftCreationOptions.ReferencedMessage.MimeMessage.References);
if (!string.IsNullOrEmpty(replyingMimeMessage.MessageId)) if (!string.IsNullOrEmpty(draftCreationOptions.ReferencedMessage.MimeMessage.MessageId))
copy.InReplyTo = replyingMimeMessage.MessageId; copy.InReplyTo = draftCreationOptions.ReferencedMessage.MimeMessage.MessageId;
if (!string.IsNullOrEmpty(replyingMailItem?.ThreadId)) if (!string.IsNullOrEmpty(draftCreationOptions.ReferencedMessage.MailCopy?.ThreadId))
copy.ThreadId = replyingMailItem.ThreadId; copy.ThreadId = draftCreationOptions.ReferencedMessage.MailCopy.ThreadId;
} }
await Connection.InsertAsync(copy); await Connection.InsertAsync(copy);
await _mimeFileService.SaveMimeMessageAsync(copy.FileId, createdDraftMimeMessage, composerAccount.Id); await _mimeFileService.SaveMimeMessageAsync(copy.FileId, createdDraftMimeMessage, composerAccount.Id);
ReportUIChange(new DraftCreated(copy, composerAccount)); ReportUIChange(new DraftCreated(copy, composerAccount));
return copy; return (copy, createdDraftMimeMessage.GetBase64MimeMessage());
} }
public async Task<List<MailCopy>> GetMailsByFolderIdAsync(Guid folderId) public async Task<List<MailCopy>> GetMailsByFolderIdAsync(Guid folderId)
@@ -629,85 +616,42 @@ namespace Wino.Core.Services
} }
} }
public async Task<string> CreateDraftMimeBase64Async(Guid accountId, DraftCreationOptions draftCreationOptions) private async Task<MimeMessage> CreateDraftMimeAsync(MailAccount account, DraftCreationOptions draftCreationOptions)
{ {
// This unique id is stored in mime headers for Wino to identify remote message with local copy. // 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. // Same unique id will be used for the local copy as well.
// Synchronizer will map this unique id to the local draft copy after synchronization. // Synchronizer will map this unique id to the local draft copy after synchronization.
var messageUniqueId = Guid.NewGuid();
var message = new MimeMessage() var message = new MimeMessage()
{ {
Headers = { { Constants.WinoLocalDraftHeader, messageUniqueId.ToString() } } Headers = { { Constants.WinoLocalDraftHeader, Guid.NewGuid().ToString() } },
From = { new MailboxAddress(account.SenderName, account.Address) }
}; };
var builder = new BodyBuilder(); var builder = new BodyBuilder();
var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false); var signature = await GetSignature(account, draftCreationOptions.Reason);
if (account == null) _ = draftCreationOptions.Reason switch
{ {
_logger.Warning("Can't create draft mime message because account {AccountId} does not exist.", accountId); DraftCreationReason.Empty => CreateEmptyDraft(builder, message, draftCreationOptions, signature),
_ => CreateReferencedDraft(builder, message, draftCreationOptions, account, signature),
};
return null; builder.SetHtmlBody(builder.HtmlBody);
}
var reason = draftCreationOptions.Reason; message.Body = builder.ToMessageBody();
var referenceMessage = draftCreationOptions.ReferenceMimeMessage;
message.From.Add(new MailboxAddress(account.SenderName, account.Address)); return message;
}
// It contains empty blocks with inlined font, to make sure when users starts typing,it will follow selected font. private string CreateHtmlGap()
var gapHtml = CreateHtmlGap(); {
var template = $"""<div style="font-family: '{_preferencesService.ComposerFont}', Arial, sans-serif; font-size: {_preferencesService.ComposerFontSize}px"><br></div>""";
return string.Concat(Enumerable.Repeat(template, 2));
}
// Manage "To" private async Task<string> GetSignature(MailAccount account, DraftCreationReason reason)
if (reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll) {
{
// Reply to the sender of the message
if (referenceMessage.ReplyTo.Count > 0)
message.To.AddRange(referenceMessage.ReplyTo);
else if (referenceMessage.From.Count > 0)
message.To.AddRange(referenceMessage.From);
else if (referenceMessage.Sender != null)
message.To.Add(referenceMessage.Sender);
if (reason == DraftCreationReason.ReplyAll)
{
// Include all of the other original recipients
message.To.AddRange(referenceMessage.To);
// Find self and remove
var self = message.To.FirstOrDefault(a => a is MailboxAddress mailboxAddress && mailboxAddress.Address == account.Address);
if (self != null)
message.To.Remove(self);
message.Cc.AddRange(referenceMessage.Cc);
}
// Manage "ThreadId-ConversationId"
if (!string.IsNullOrEmpty(referenceMessage.MessageId))
{
message.InReplyTo = referenceMessage.MessageId;
message.References.AddRange(referenceMessage.References);
message.References.Add(referenceMessage.MessageId);
}
message.Headers.Add("Thread-Topic", referenceMessage.Subject);
builder.HtmlBody = CreateHtmlForReferencingMessage(referenceMessage);
}
if (reason == DraftCreationReason.Forward)
{
builder.HtmlBody = CreateHtmlForReferencingMessage(referenceMessage);
}
// Append signatures if needed.
if (account.Preferences.IsSignatureEnabled) if (account.Preferences.IsSignatureEnabled)
{ {
var signatureId = reason == DraftCreationReason.Empty ? var signatureId = reason == DraftCreationReason.Empty ?
@@ -718,26 +662,88 @@ namespace Wino.Core.Services
{ {
var signature = await _signatureService.GetSignatureAsync(signatureId.Value); var signature = await _signatureService.GetSignatureAsync(signatureId.Value);
if (string.IsNullOrWhiteSpace(builder.HtmlBody)) return signature.HtmlBody;
{
builder.HtmlBody = $"{gapHtml}{signature.HtmlBody}";
}
else
{
builder.HtmlBody = $"{gapHtml}{signature.HtmlBody}{gapHtml}{builder.HtmlBody}";
}
} }
} }
else
return null;
}
private MimeMessage CreateEmptyDraft(BodyBuilder builder, MimeMessage message, DraftCreationOptions draftCreationOptions, string signature)
{
builder.HtmlBody = CreateHtmlGap();
if (draftCreationOptions.MailToUri != null)
{ {
builder.HtmlBody = $"{gapHtml}{builder.HtmlBody}"; if (draftCreationOptions.MailToUri.Subject != null)
message.Subject = draftCreationOptions.MailToUri.Subject;
if (draftCreationOptions.MailToUri.Body != null)
{
builder.HtmlBody = $"""<div style="font-family: '{_preferencesService.ComposerFont}', Arial, sans-serif; font-size: {_preferencesService.ComposerFontSize}px">{draftCreationOptions.MailToUri.Body}</div>""" + builder.HtmlBody;
}
if (draftCreationOptions.MailToUri.To.Any())
message.To.AddRange(draftCreationOptions.MailToUri.To.Select(x => new MailboxAddress(x, x)));
if (draftCreationOptions.MailToUri.Cc.Any())
message.Cc.AddRange(draftCreationOptions.MailToUri.Cc.Select(x => new MailboxAddress(x, x)));
if (draftCreationOptions.MailToUri.Bcc.Any())
message.Bcc.AddRange(draftCreationOptions.MailToUri.Bcc.Select(x => new MailboxAddress(x, x)));
}
if (signature != null)
builder.HtmlBody += signature;
return message;
}
private MimeMessage CreateReferencedDraft(BodyBuilder builder, MimeMessage message, DraftCreationOptions draftCreationOptions, MailAccount account, string signature)
{
var reason = draftCreationOptions.Reason;
var referenceMessage = draftCreationOptions.ReferencedMessage.MimeMessage;
var gap = CreateHtmlGap();
builder.HtmlBody = gap + CreateHtmlForReferencingMessage(referenceMessage);
if (signature != null)
{
builder.HtmlBody = gap + signature + builder.HtmlBody;
}
// Manage "To"
if (reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll)
{
// Reply to the sender of the message
if (referenceMessage.ReplyTo.Count > 0)
message.To.AddRange(referenceMessage.ReplyTo);
else if (referenceMessage.From.Count > 0)
message.To.AddRange(referenceMessage.From);
else if (referenceMessage.Sender != null)
message.To.Add(referenceMessage.Sender);
if (reason == DraftCreationReason.ReplyAll)
{
// Include all of the other original recipients
message.To.AddRange(referenceMessage.To.Where(x => x is MailboxAddress mailboxAddress && !mailboxAddress.Address.Equals(account.Address, StringComparison.OrdinalIgnoreCase)));
message.Cc.AddRange(referenceMessage.Cc.Where(x => x is MailboxAddress mailboxAddress && !mailboxAddress.Address.Equals(account.Address, StringComparison.OrdinalIgnoreCase)));
}
// Manage "ThreadId-ConversationId"
if (!string.IsNullOrEmpty(referenceMessage.MessageId))
{
message.InReplyTo = referenceMessage.MessageId;
message.References.AddRange(referenceMessage.References);
message.References.Add(referenceMessage.MessageId);
}
message.Headers.Add("Thread-Topic", referenceMessage.Subject);
} }
// Manage Subject // Manage Subject
if (reason == DraftCreationReason.Forward && !referenceMessage.Subject.StartsWith("FW: ", StringComparison.OrdinalIgnoreCase)) if (reason == DraftCreationReason.Forward && !referenceMessage.Subject.StartsWith("FW: ", StringComparison.OrdinalIgnoreCase))
message.Subject = $"FW: {referenceMessage.Subject}"; message.Subject = $"FW: {referenceMessage.Subject}";
else if ((reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll) && else if ((reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll) && !referenceMessage.Subject.StartsWith("RE: ", StringComparison.OrdinalIgnoreCase))
!referenceMessage.Subject.StartsWith("RE: ", StringComparison.OrdinalIgnoreCase))
message.Subject = $"RE: {referenceMessage.Subject}"; message.Subject = $"RE: {referenceMessage.Subject}";
else if (referenceMessage != null) else if (referenceMessage != null)
message.Subject = referenceMessage.Subject; message.Subject = referenceMessage.Subject;
@@ -751,63 +757,7 @@ namespace Wino.Core.Services
} }
} }
if (!string.IsNullOrEmpty(builder.HtmlBody)) return message;
{
builder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(builder.HtmlBody);
}
message.Body = builder.ToMessageBody();
// Apply mail-to protocol parameters if exists.
if (draftCreationOptions.MailtoParameters != null)
{
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoSubjectParameterKey, out string subjectParameter))
message.Subject = subjectParameter;
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoBodyParameterKey, out string bodyParameter))
{
builder.TextBody = bodyParameter;
builder.HtmlBody = bodyParameter;
message.Body = builder.ToMessageBody();
}
static InternetAddressList ExtractRecipients(string parameterValue)
{
var list = new InternetAddressList();
var splittedRecipients = parameterValue.Split(',');
foreach (var recipient in splittedRecipients)
list.Add(new MailboxAddress(recipient, recipient));
return list;
}
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoToParameterKey, out string toParameter))
message.To.AddRange(ExtractRecipients(toParameter));
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoCCParameterKey, out string ccParameter))
message.Cc.AddRange(ExtractRecipients(ccParameter));
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoBCCParameterKey, out string bccParameter))
message.Bcc.AddRange(ExtractRecipients(bccParameter));
}
else
{
// Update TextBody from existing HtmlBody if exists.
}
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. // Generates html representation of To/Cc/From/Time and so on from referenced message.
string CreateHtmlForReferencingMessage(MimeMessage referenceMessage) string CreateHtmlForReferencingMessage(MimeMessage referenceMessage)
@@ -820,28 +770,22 @@ namespace Wino.Core.Services
visitor.Visit(referenceMessage); visitor.Visit(referenceMessage);
htmlMimeInfo += $""" htmlMimeInfo += $"""
<div id="divRplyFwdMsg" dir="ltr"> <div id="divRplyFwdMsg" dir="ltr">
<font face="Calibri, sans-serif" style="font-size: 11pt;" color="#000000"> <font face="Calibri, sans-serif" style="font-size: 11pt;" color="#000000">
<b>From:</b> {ParticipantsToHtml(referenceMessage.From)}<br> <b>From:</b> {ParticipantsToHtml(referenceMessage.From)}<br>
<b>Sent:</b> {referenceMessage.Date.ToLocalTime()}<br> <b>Sent:</b> {referenceMessage.Date.ToLocalTime()}<br>
<b>To:</b> {ParticipantsToHtml(referenceMessage.To)}<br> <b>To:</b> {ParticipantsToHtml(referenceMessage.To)}<br>
{(referenceMessage.Cc.Count > 0 ? $"<b>Cc:</b> {ParticipantsToHtml(referenceMessage.Cc)}<br>" : string.Empty)} {(referenceMessage.Cc.Count > 0 ? $"<b>Cc:</b> {ParticipantsToHtml(referenceMessage.Cc)}<br>" : string.Empty)}
<b>Subject:</b> {referenceMessage.Subject} <b>Subject:</b> {referenceMessage.Subject}
</font> </font>
<div>&nbsp;</div> <div>&nbsp;</div>
{visitor.HtmlBody} {visitor.HtmlBody}
</div> </div>
"""; """;
return htmlMimeInfo; return htmlMimeInfo;
} }
string CreateHtmlGap()
{
var template = $"""<div style="font-family: '{_preferencesService.ComposerFont}', Arial, sans-serif; font-size: {_preferencesService.ComposerFontSize}px"><br></div>""";
return string.Concat(Enumerable.Repeat(template, 5));
}
static string ParticipantsToHtml(InternetAddressList internetAddresses) => static string ParticipantsToHtml(InternetAddressList internetAddresses) =>
string.Join("; ", internetAddresses.Mailboxes string.Join("; ", internetAddresses.Mailboxes
.Select(x => $"{x.Name ?? Translator.UnknownSender} &lt;<a href=\"mailto:{x.Address ?? Translator.UnknownAddress}\">{x.Address ?? Translator.UnknownAddress}</a>&gt;")); .Select(x => $"{x.Name ?? Translator.UnknownSender} &lt;<a href=\"mailto:{x.Address ?? Translator.UnknownAddress}\">{x.Address ?? Translator.UnknownAddress}</a>&gt;"));

View File

@@ -1,8 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Newtonsoft.Json;
using Serilog; using Serilog;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
@@ -44,7 +44,7 @@ namespace Wino.Core.Services
var stremValue = await new StreamReader(resourceStream).ReadToEndAsync().ConfigureAwait(false); var stremValue = await new StreamReader(resourceStream).ReadToEndAsync().ConfigureAwait(false);
var translationLookups = JsonConvert.DeserializeObject<Dictionary<string, string>>(stremValue); var translationLookups = JsonSerializer.Deserialize<Dictionary<string, string>>(stremValue);
// Insert new translation key-value pairs. // Insert new translation key-value pairs.
// Overwrite existing values for the same keys. // Overwrite existing values for the same keys.

View File

@@ -80,7 +80,7 @@ namespace Wino.Core.Services
await QueueRequestAsync(accountRequest, accountId.Key); await QueueRequestAsync(accountRequest, accountId.Key);
} }
QueueSynchronization(accountId.Key); await QueueSynchronizationAsync(accountId.Key);
} }
} }
@@ -108,15 +108,15 @@ namespace Wino.Core.Services
if (request == null) return; if (request == null) return;
await QueueRequestAsync(request, accountId); await QueueRequestAsync(request, accountId);
QueueSynchronization(accountId); await QueueSynchronizationAsync(accountId);
} }
public async Task ExecuteAsync(DraftPreperationRequest draftPreperationRequest) public async Task ExecuteAsync(DraftPreparationRequest draftPreperationRequest)
{ {
var request = new CreateDraftRequest(draftPreperationRequest); var request = new CreateDraftRequest(draftPreperationRequest);
await QueueRequestAsync(request, draftPreperationRequest.Account.Id); await QueueRequestAsync(request, draftPreperationRequest.Account.Id);
QueueSynchronization(draftPreperationRequest.Account.Id); await QueueSynchronizationAsync(draftPreperationRequest.Account.Id);
} }
public async Task ExecuteAsync(SendDraftPreparationRequest sendDraftPreperationRequest) public async Task ExecuteAsync(SendDraftPreparationRequest sendDraftPreperationRequest)
@@ -124,23 +124,26 @@ namespace Wino.Core.Services
var request = new SendDraftRequest(sendDraftPreperationRequest); var request = new SendDraftRequest(sendDraftPreperationRequest);
await QueueRequestAsync(request, sendDraftPreperationRequest.MailItem.AssignedAccount.Id); await QueueRequestAsync(request, sendDraftPreperationRequest.MailItem.AssignedAccount.Id);
QueueSynchronization(sendDraftPreperationRequest.MailItem.AssignedAccount.Id); await QueueSynchronizationAsync(sendDraftPreperationRequest.MailItem.AssignedAccount.Id);
} }
private async Task QueueRequestAsync(IRequestBase request, Guid accountId) private async Task QueueRequestAsync(IRequestBase request, Guid accountId)
{ {
try try
{ {
await EnsureServerConnectedAsync();
await _winoServerConnectionManager.QueueRequestAsync(request, accountId); await _winoServerConnectionManager.QueueRequestAsync(request, accountId);
} }
catch (WinoServerException serverException) catch (WinoServerException serverException)
{ {
_dialogService.InfoBarMessage("", serverException.Message, InfoBarMessageType.Error); _dialogService.InfoBarMessage("Wino Server Exception", serverException.Message, InfoBarMessageType.Error);
} }
} }
private void QueueSynchronization(Guid accountId) private async Task QueueSynchronizationAsync(Guid accountId)
{ {
await EnsureServerConnectedAsync();
var options = new SynchronizationOptions() var options = new SynchronizationOptions()
{ {
AccountId = accountId, AccountId = accountId,
@@ -149,5 +152,12 @@ namespace Wino.Core.Services
WeakReferenceMessenger.Default.Send(new NewSynchronizationRequested(options, SynchronizationSource.Client)); WeakReferenceMessenger.Default.Send(new NewSynchronizationRequested(options, SynchronizationSource.Client));
} }
private async Task EnsureServerConnectedAsync()
{
if (_winoServerConnectionManager.Status == WinoServerConnectionStatus.Connected) return;
await _winoServerConnectionManager.ConnectAsync();
}
} }
} }

View File

@@ -922,7 +922,7 @@ namespace Wino.Core.Synchronizers
} }
// In case of the high input, we'll batch them by 50 to reflect changes quickly. // In case of the high input, we'll batch them by 50 to reflect changes quickly.
var batchedMissingMailIds = missingMailIds.Batch(50).Select(a => new UniqueIdSet(a, SortOrder.Descending)); var batchedMissingMailIds = missingMailIds.Batch(50).Select(a => new UniqueIdSet(a, SortOrder.Ascending));
foreach (var batchMissingMailIds in batchedMissingMailIds) foreach (var batchMissingMailIds in batchedMissingMailIds)
{ {

View File

@@ -31,7 +31,6 @@
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.62.0" /> <PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.62.0" />
<PackageReference Include="MimeKit" Version="4.7.1" /> <PackageReference Include="MimeKit" Version="4.7.1" />
<PackageReference Include="morelinq" Version="4.1.0" /> <PackageReference Include="morelinq" Version="4.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" /> <PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
<PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" /> <PackageReference Include="Serilog.Exceptions" Version="8.4.0" />

View File

@@ -236,14 +236,14 @@ namespace Wino.Mail.ViewModels
await ProcessLaunchOptionsAsync(); await ProcessLaunchOptionsAsync();
await ForceAllAccountSynchronizationsAsync(); await ForceAllAccountSynchronizationsAsync();
await ConfigureBackgroundTasksAsync(); ConfigureBackgroundTasks();
} }
private async Task ConfigureBackgroundTasksAsync() private void ConfigureBackgroundTasks()
{ {
try try
{ {
await _backgroundTaskService.HandleBackgroundTaskRegistrations(); _backgroundTaskService.UnregisterAllBackgroundTask();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -302,7 +302,7 @@ namespace Wino.Mail.ViewModels
} }
else else
{ {
bool hasMailtoActivation = _launchProtocolService.MailtoParameters != null; bool hasMailtoActivation = _launchProtocolService.MailToUri != null;
if (hasMailtoActivation) if (hasMailtoActivation)
{ {
@@ -774,16 +774,13 @@ namespace Wino.Mail.ViewModels
var draftOptions = new DraftCreationOptions var draftOptions = new DraftCreationOptions
{ {
Reason = DraftCreationReason.Empty, Reason = DraftCreationReason.Empty,
MailToUri = _launchProtocolService.MailToUri
// Include mail to parameters for parsing mailto if any.
MailtoParameters = _launchProtocolService.MailtoParameters
}; };
var createdBase64EncodedMimeMessage = await _mailService.CreateDraftMimeBase64Async(account.Id, draftOptions).ConfigureAwait(false); var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(account.Id, draftOptions).ConfigureAwait(false);
var createdDraftMailMessage = await _mailService.CreateDraftAsync(account, createdBase64EncodedMimeMessage).ConfigureAwait(false);
var draftPreperationRequest = new DraftPreperationRequest(account, createdDraftMailMessage, createdBase64EncodedMimeMessage); var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage);
await _winoRequestDelegator.ExecuteAsync(draftPreperationRequest); await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest);
} }
protected override async void OnAccountUpdated(MailAccount updatedAccount) protected override async void OnAccountUpdated(MailAccount updatedAccount)

View File

@@ -3,12 +3,12 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using MimeKit; using MimeKit;
using MimeKit.Utils;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
@@ -58,7 +58,7 @@ namespace Wino.Mail.ViewModels
private MessageImportance selectedMessageImportance; private MessageImportance selectedMessageImportance;
[ObservableProperty] [ObservableProperty]
private bool isCCBCCVisible = true; private bool isCCBCCVisible;
[ObservableProperty] [ObservableProperty]
private string subject; private string subject;
@@ -77,21 +77,20 @@ namespace Wino.Mail.ViewModels
[ObservableProperty] [ObservableProperty]
private bool isDraggingOverImagesDropZone; private bool isDraggingOverImagesDropZone;
public ObservableCollection<MailAttachmentViewModel> IncludedAttachments { get; set; } = new ObservableCollection<MailAttachmentViewModel>(); public ObservableCollection<MailAttachmentViewModel> IncludedAttachments { get; set; } = [];
public ObservableCollection<MailAccount> Accounts { get; set; } = [];
public ObservableCollection<MailAccount> Accounts { get; set; } = new ObservableCollection<MailAccount>(); public ObservableCollection<AddressInformation> ToItems { get; set; } = [];
public ObservableCollection<AddressInformation> ToItems { get; set; } = new ObservableCollection<AddressInformation>(); public ObservableCollection<AddressInformation> CCItems { get; set; } = [];
public ObservableCollection<AddressInformation> CCItemsItems { get; set; } = new ObservableCollection<AddressInformation>(); public ObservableCollection<AddressInformation> BCCItems { get; set; } = [];
public ObservableCollection<AddressInformation> BCCItems { get; set; } = new ObservableCollection<AddressInformation>();
public List<EditorToolbarSection> ToolbarSections { get; set; } = new List<EditorToolbarSection>() public List<EditorToolbarSection> ToolbarSections { get; set; } =
{ [
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Format }, new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Format },
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Insert }, new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Insert },
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Draw }, new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Draw },
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Options } new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Options }
}; ];
private EditorToolbarSection selectedToolbarSection; private EditorToolbarSection selectedToolbarSection;
@@ -190,7 +189,7 @@ namespace Wino.Mail.ViewModels
// Save recipients. // Save recipients.
SaveAddressInfo(ToItems, CurrentMimeMessage.To); SaveAddressInfo(ToItems, CurrentMimeMessage.To);
SaveAddressInfo(CCItemsItems, CurrentMimeMessage.Cc); SaveAddressInfo(CCItems, CurrentMimeMessage.Cc);
SaveAddressInfo(BCCItems, CurrentMimeMessage.Bcc); SaveAddressInfo(BCCItems, CurrentMimeMessage.Bcc);
SaveImportance(); SaveImportance();
@@ -239,17 +238,9 @@ namespace Wino.Mail.ViewModels
{ {
if (GetHTMLBodyFunction != null) if (GetHTMLBodyFunction != null)
{ {
var htmlBody = await GetHTMLBodyFunction(); bodyBuilder.SetHtmlBody(await GetHTMLBodyFunction());
if (!string.IsNullOrEmpty(htmlBody))
{
bodyBuilder.HtmlBody = Regex.Unescape(htmlBody);
}
} }
if (!string.IsNullOrEmpty(bodyBuilder.HtmlBody))
bodyBuilder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(bodyBuilder.HtmlBody);
if (bodyBuilder.HtmlBody != null && bodyBuilder.TextBody != null) if (bodyBuilder.HtmlBody != null && bodyBuilder.TextBody != null)
CurrentMimeMessage.Body = bodyBuilder.ToMessageBody(); CurrentMimeMessage.Body = bodyBuilder.ToMessageBody();
} }
@@ -309,7 +300,7 @@ namespace Wino.Mail.ViewModels
// Check if there is any delivering mail address from protocol launch. // Check if there is any delivering mail address from protocol launch.
if (_launchProtocolService.MailtoParameters != null) if (_launchProtocolService.MailToUri != null)
{ {
// TODO // TODO
//var requestedMailContact = await GetAddressInformationAsync(_launchProtocolService.MailtoParameters, ToItems); //var requestedMailContact = await GetAddressInformationAsync(_launchProtocolService.MailtoParameters, ToItems);
@@ -322,7 +313,7 @@ namespace Wino.Mail.ViewModels
// DialogService.InfoBarMessage("Invalid Address", "Address is not a valid e-mail address.", InfoBarMessageType.Warning); // DialogService.InfoBarMessage("Invalid Address", "Address is not a valid e-mail address.", InfoBarMessageType.Warning);
// Clear the address. // Clear the address.
_launchProtocolService.MailtoParameters = null; _launchProtocolService.MailToUri = null;
} }
} }
@@ -408,7 +399,6 @@ namespace Wino.Mail.ViewModels
{ {
DialogService.InfoBarMessage("Busy", "Mail is being processed. Please wait a moment and try again.", InfoBarMessageType.Warning); DialogService.InfoBarMessage("Busy", "Mail is being processed. Please wait a moment and try again.", InfoBarMessageType.Warning);
} }
catch (ComposerMimeNotFoundException) catch (ComposerMimeNotFoundException)
{ {
DialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error); DialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error);
@@ -427,15 +417,18 @@ namespace Wino.Mail.ViewModels
// Extract information // Extract information
ToItems.Clear(); ToItems.Clear();
CCItemsItems.Clear(); CCItems.Clear();
BCCItems.Clear(); BCCItems.Clear();
LoadAddressInfo(replyingMime.To, ToItems); LoadAddressInfo(replyingMime.To, ToItems);
LoadAddressInfo(replyingMime.Cc, CCItemsItems); LoadAddressInfo(replyingMime.Cc, CCItems);
LoadAddressInfo(replyingMime.Bcc, BCCItems); LoadAddressInfo(replyingMime.Bcc, BCCItems);
LoadAttachments(replyingMime.Attachments); LoadAttachments(replyingMime.Attachments);
if (replyingMime.Cc.Any() || replyingMime.Bcc.Any())
IsCCBCCVisible = true;
Subject = replyingMime.Subject; Subject = replyingMime.Subject;
CurrentMimeMessage = replyingMime; CurrentMimeMessage = replyingMime;

View File

@@ -610,6 +610,9 @@ namespace Wino.Mail.ViewModels
if (ActiveFolder == null) return; if (ActiveFolder == null) return;
// At least accounts must match.
if (ActiveFolder.HandlingFolders.Any(a => a.MailAccountId != addedMail.AssignedAccount.Id)) return;
// Messages coming to sent or draft folder must be inserted regardless of the filter. // Messages coming to sent or draft folder must be inserted regardless of the filter.
bool shouldPreventIgnoringFilter = addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft || bool shouldPreventIgnoringFilter = addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft ||
addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent; addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent;
@@ -617,6 +620,7 @@ namespace Wino.Mail.ViewModels
// Item does not belong to this folder and doesn't have special type to be inserted. // Item does not belong to this folder and doesn't have special type to be inserted.
if (!shouldPreventIgnoringFilter && !ActiveFolder.HandlingFolders.Any(a => a.Id == addedMail.AssignedFolder.Id)) return; if (!shouldPreventIgnoringFilter && !ActiveFolder.HandlingFolders.Any(a => a.Id == addedMail.AssignedFolder.Id)) return;
// Item should be prevented from being added to the list due to filter.
if (!shouldPreventIgnoringFilter && ShouldPreventItemAdd(addedMail)) return; if (!shouldPreventIgnoringFilter && ShouldPreventItemAdd(addedMail)) return;
await MailCollection.AddAsync(addedMail); await MailCollection.AddAsync(addedMail);
@@ -845,9 +849,6 @@ namespace Wino.Mail.ViewModels
trackingSynchronizationId = null; trackingSynchronizationId = null;
completedTrackingSynchronizationCount = 0; completedTrackingSynchronizationCount = 0;
// Check whether the account synchronizer that this folder belongs to is already in synchronization.
await CheckIfAccountIsSynchronizingAsync();
// Notify change for archive-unarchive app bar button. // Notify change for archive-unarchive app bar button.
OnPropertyChanged(nameof(IsArchiveSpecialFolder)); OnPropertyChanged(nameof(IsArchiveSpecialFolder));
@@ -865,6 +866,9 @@ namespace Wino.Mail.ViewModels
await Task.Delay(100); await Task.Delay(100);
} }
// Check whether the account synchronizer that this folder belongs to is already in synchronization.
await CheckIfAccountIsSynchronizingAsync();
// Let awaiters know about the completion of mail init. // Let awaiters know about the completion of mail init.
message.FolderInitLoadAwaitTask?.TrySetResult(true); message.FolderInitLoadAwaitTask?.TrySetResult(true);

View File

@@ -117,7 +117,7 @@ namespace Wino.Mail.ViewModels
#endregion #endregion
public INativeAppService NativeAppService { get; } public INativeAppService NativeAppService { get; }
public IStatePersistanceService StatePersistanceService { get; } public IStatePersistanceService StatePersistenceService { get; }
public IPreferencesService PreferencesService { get; } public IPreferencesService PreferencesService { get; }
public MailRenderingPageViewModel(IDialogService dialogService, public MailRenderingPageViewModel(IDialogService dialogService,
@@ -127,14 +127,14 @@ namespace Wino.Mail.ViewModels
Core.Domain.Interfaces.IMailService mailService, Core.Domain.Interfaces.IMailService mailService,
IFileService fileService, IFileService fileService,
IWinoRequestDelegator requestDelegator, IWinoRequestDelegator requestDelegator,
IStatePersistanceService statePersistanceService, IStatePersistanceService statePersistenceService,
IClipboardService clipboardService, IClipboardService clipboardService,
IUnsubscriptionService unsubscriptionService, IUnsubscriptionService unsubscriptionService,
IPreferencesService preferencesService, IPreferencesService preferencesService,
IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService) IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService)
{ {
NativeAppService = nativeAppService; NativeAppService = nativeAppService;
StatePersistanceService = statePersistanceService; StatePersistenceService = statePersistenceService;
PreferencesService = preferencesService; PreferencesService = preferencesService;
_winoServerConnectionManager = winoServerConnectionManager; _winoServerConnectionManager = winoServerConnectionManager;
_clipboardService = clipboardService; _clipboardService = clipboardService;
@@ -255,37 +255,27 @@ namespace Wino.Mail.ViewModels
if (initializedMailItemViewModel == null) return; if (initializedMailItemViewModel == null) return;
// Create new draft. // Create new draft.
var draftOptions = new DraftCreationOptions(); var draftOptions = new DraftCreationOptions()
if (operation == MailOperation.Reply)
draftOptions.Reason = DraftCreationReason.Reply;
else if (operation == MailOperation.ReplyAll)
draftOptions.Reason = DraftCreationReason.ReplyAll;
else if (operation == MailOperation.Forward)
draftOptions.Reason = DraftCreationReason.Forward;
// TODO: Separate mailto related stuff out of DraftCreationOptions and provide better
// model for draft preperation request. Right now it's a mess.
draftOptions.ReferenceMailCopy = initializedMailItemViewModel.MailCopy;
draftOptions.ReferenceMimeMessage = initializedMimeMessageInformation.MimeMessage;
var createdMimeMessage = await _mailService.CreateDraftMimeBase64Async(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
var createdDraftMailMessage = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount,
createdMimeMessage,
initializedMimeMessageInformation.MimeMessage,
initializedMailItemViewModel).ConfigureAwait(false);
var draftPreperationRequest = new DraftPreperationRequest(initializedMailItemViewModel.AssignedAccount,
createdDraftMailMessage,
createdMimeMessage)
{ {
ReferenceMimeMessage = initializedMimeMessageInformation.MimeMessage, Reason = operation switch
ReferenceMailCopy = initializedMailItemViewModel.MailCopy {
MailOperation.Reply => DraftCreationReason.Reply,
MailOperation.ReplyAll => DraftCreationReason.ReplyAll,
MailOperation.Forward => DraftCreationReason.Forward,
_ => DraftCreationReason.Empty
},
ReferencedMessage = new ReferencedMessage()
{
MimeMessage = initializedMimeMessageInformation.MimeMessage,
MailCopy = initializedMailItemViewModel.MailCopy
}
}; };
await _requestDelegator.ExecuteAsync(draftPreperationRequest); var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, initializedMailItemViewModel.MailCopy);
await _requestDelegator.ExecuteAsync(draftPreparationRequest);
} }
else if (initializedMailItemViewModel != null) else if (initializedMailItemViewModel != null)
@@ -453,7 +443,7 @@ namespace Wino.Mail.ViewModels
OnPropertyChanged(nameof(IsImageRenderingDisabled)); OnPropertyChanged(nameof(IsImageRenderingDisabled));
StatePersistanceService.IsReadingMail = true; StatePersistenceService.IsReadingMail = true;
}); });
} }
@@ -477,7 +467,7 @@ namespace Wino.Mail.ViewModels
Attachments.Clear(); Attachments.Clear();
MenuItems.Clear(); MenuItems.Clear();
StatePersistanceService.IsReadingMail = false; StatePersistenceService.IsReadingMail = false;
} }
private void LoadAddressInfo(InternetAddressList list, ObservableCollection<AddressInformation> collection) private void LoadAddressInfo(InternetAddressList list, ObservableCollection<AddressInformation> collection)

View File

@@ -2,6 +2,7 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Toolkit.Uwp.Helpers;
using Windows.ApplicationModel.Activation; using Windows.ApplicationModel.Activation;
using Windows.Storage; using Windows.Storage;
using Windows.UI.Xaml; using Windows.UI.Xaml;
@@ -9,7 +10,6 @@ using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation; using Windows.UI.Xaml.Media.Animation;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Services; using Wino.Core.Services;
using Wino.Helpers;
using Wino.Views; using Wino.Views;
namespace Wino.Activation namespace Wino.Activation

View File

@@ -1,8 +1,9 @@
using System.Threading.Tasks; using System;
using System.Web; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Windows.ApplicationModel.Activation; using Windows.ApplicationModel.Activation;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Launch;
using Wino.Messaging.Client.Authorization; using Wino.Messaging.Client.Authorization;
using Wino.Messaging.Client.Shell; using Wino.Messaging.Client.Shell;
@@ -36,11 +37,7 @@ namespace Wino.Activation
else if (protocolString.StartsWith(MailtoProtocolTag)) else if (protocolString.StartsWith(MailtoProtocolTag))
{ {
// mailto activation. Try to parse params. // mailto activation. Try to parse params.
_launchProtocolService.MailToUri = new MailToUri(protocolString);
var replaced = protocolString.Replace(MailtoProtocolTag, "mailto=");
replaced = Wino.Core.Extensions.StringExtensions.ReplaceFirst(replaced, "?", "&");
_launchProtocolService.MailtoParameters = HttpUtility.ParseQueryString(replaced);
if (_nativeAppService.IsAppRunning()) if (_nativeAppService.IsAppRunning())
{ {
@@ -51,5 +48,21 @@ namespace Wino.Activation
return Task.CompletedTask; return Task.CompletedTask;
} }
protected override bool CanHandleInternal(ProtocolActivatedEventArgs args)
{
// Validate the URI scheme.
try
{
var uriGet = args.Uri;
}
catch (UriFormatException)
{
return false;
}
return base.CanHandleInternal(args);
}
} }
} }

View File

@@ -10,6 +10,7 @@ using Microsoft.AppCenter;
using Microsoft.AppCenter.Analytics; using Microsoft.AppCenter.Analytics;
using Microsoft.AppCenter.Crashes; using Microsoft.AppCenter.Crashes;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Nito.AsyncEx;
using Serilog; using Serilog;
using Windows.ApplicationModel; using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation; using Windows.ApplicationModel.Activation;
@@ -66,7 +67,6 @@ namespace Wino
private List<IInitializeAsync> initializeServices => new List<IInitializeAsync>() private List<IInitializeAsync> initializeServices => new List<IInitializeAsync>()
{ {
_databaseService, _databaseService,
_appServiceConnectionManager,
_translationService, _translationService,
_themeService, _themeService,
}; };
@@ -76,8 +76,6 @@ namespace Wino
InitializeComponent(); InitializeComponent();
UnhandledException += OnAppUnhandledException; UnhandledException += OnAppUnhandledException;
EnteredBackground += OnEnteredBackground;
LeavingBackground += OnLeavingBackground;
Resuming += OnResuming; Resuming += OnResuming;
Suspending += OnSuspending; Suspending += OnSuspending;
@@ -125,8 +123,6 @@ namespace Wino
} }
private void LogActivation(string log) => Log.Information($"{WinoLaunchLogPrefix}{log}"); private void LogActivation(string log) => Log.Information($"{WinoLaunchLogPrefix}{log}");
private void OnLeavingBackground(object sender, LeavingBackgroundEventArgs e) => LogActivation($"Wino went foreground.");
private void OnEnteredBackground(object sender, EnteredBackgroundEventArgs e) => LogActivation($"Wino went background.");
private IServiceProvider ConfigureServices() private IServiceProvider ConfigureServices()
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
@@ -146,7 +142,6 @@ namespace Wino
private void RegisterActivationHandlers(IServiceCollection services) private void RegisterActivationHandlers(IServiceCollection services)
{ {
services.AddTransient<ProtocolActivationHandler>(); services.AddTransient<ProtocolActivationHandler>();
// services.AddTransient<BackgroundActivationHandler>();
services.AddTransient<ToastNotificationActivationHandler>(); services.AddTransient<ToastNotificationActivationHandler>();
services.AddTransient<FileActivationHandler>(); services.AddTransient<FileActivationHandler>();
} }
@@ -283,19 +278,18 @@ namespace Wino
_appServiceConnectionManager.Connection = appServiceTriggerDetails.AppServiceConnection; _appServiceConnectionManager.Connection = appServiceTriggerDetails.AppServiceConnection;
WeakReferenceMessenger.Default.Send(new WinoServerConnectionEstrablished()); WeakReferenceMessenger.Default.Send(new WinoServerConnectionEstablished());
} }
} }
else if (args.TaskInstance.TriggerDetails is ToastNotificationActionTriggerDetail toastNotificationActionTriggerDetail) else if (args.TaskInstance.TriggerDetails is ToastNotificationActionTriggerDetail toastNotificationActionTriggerDetail)
{ {
await InitializeServicesAsync();
// Notification action is triggered and the app is not running. // Notification action is triggered and the app is not running.
toastActionBackgroundTaskDeferral = args.TaskInstance.GetDeferral(); toastActionBackgroundTaskDeferral = args.TaskInstance.GetDeferral();
args.TaskInstance.Canceled += OnToastActionClickedBackgroundTaskCanceled; args.TaskInstance.Canceled += OnToastActionClickedBackgroundTaskCanceled;
await InitializeServicesAsync();
var toastArguments = ToastArguments.Parse(toastNotificationActionTriggerDetail.Argument); var toastArguments = ToastArguments.Parse(toastNotificationActionTriggerDetail.Argument);
// All toast activation mail actions are handled here like mark as read or delete. // All toast activation mail actions are handled here like mark as read or delete.
@@ -364,13 +358,7 @@ namespace Wino
private bool IsInteractiveLaunchArgs(object args) => args is IActivatedEventArgs; private bool IsInteractiveLaunchArgs(object args) => args is IActivatedEventArgs;
private async Task InitializeServicesAsync() private Task InitializeServicesAsync() => initializeServices.Select(a => a.InitializeAsync()).WhenAll();
{
foreach (var service in initializeServices)
{
await service.InitializeAsync();
}
}
private async Task ActivateWinoAsync(object args) private async Task ActivateWinoAsync(object args)
{ {
@@ -424,13 +412,11 @@ namespace Wino
yield return Services.GetService<FileActivationHandler>(); yield return Services.GetService<FileActivationHandler>();
} }
public async void OnConnectionBackgroundTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason) public void OnConnectionBackgroundTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
{ {
sender.Canceled -= OnConnectionBackgroundTaskCanceled; sender.Canceled -= OnConnectionBackgroundTaskCanceled;
Log.Information($"Background task {sender.Task.Name} was canceled. Reason: {reason}"); Log.Information($"Server connection background task was canceled. Reason: {reason}");
await _appServiceConnectionManager.DisconnectAsync();
connectionBackgroundTaskDeferral?.Complete(); connectionBackgroundTaskDeferral?.Complete();
connectionBackgroundTaskDeferral = null; connectionBackgroundTaskDeferral = null;

View File

@@ -197,10 +197,7 @@ namespace Wino.Controls.Advanced
private void ReconnectClicked(object sender, RoutedEventArgs e) private void ReconnectClicked(object sender, RoutedEventArgs e)
{ {
// Close the popup for reconnect button. // Close the popup for reconnect button.
if (sender is Button senderButton && senderButton.Flyout is Flyout senderButtonFlyout) ReconnectFlyout.Hide();
{
senderButtonFlyout.Hide();
}
// Execute the reconnect command. // Execute the reconnect command.
ReconnectCommand?.Execute(null); ReconnectCommand?.Execute(null);

View File

@@ -1,9 +1,9 @@
using System; using System;
using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Newtonsoft.Json;
using Windows.UI.ViewManagement.Core; using Windows.UI.ViewManagement.Core;
using Windows.UI.Xaml; using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
@@ -78,7 +78,7 @@ namespace Wino.Dialogs
{ {
var editorContent = await InvokeScriptSafeAsync("GetHTMLContent();"); var editorContent = await InvokeScriptSafeAsync("GetHTMLContent();");
return JsonConvert.DeserializeObject<string>(editorContent); return JsonSerializer.Deserialize<string>(editorContent);
}); });
var underlyingThemeService = App.Current.Services.GetService<IUnderlyingThemeService>(); var underlyingThemeService = App.Current.Services.GetService<IUnderlyingThemeService>();
@@ -193,7 +193,7 @@ namespace Wino.Dialogs
string script = functionName + "("; string script = functionName + "(";
for (int i = 0; i < parameters.Length; i++) for (int i = 0; i < parameters.Length; i++)
{ {
script += JsonConvert.SerializeObject(parameters[i]); script += JsonSerializer.Serialize(parameters[i]);
if (i < parameters.Length - 1) if (i < parameters.Length - 1)
{ {
script += ", "; script += ", ";
@@ -327,7 +327,7 @@ namespace Wino.Dialogs
private void ScriptMessageReceived(CoreWebView2 sender, CoreWebView2WebMessageReceivedEventArgs args) private void ScriptMessageReceived(CoreWebView2 sender, CoreWebView2WebMessageReceivedEventArgs args)
{ {
var change = JsonConvert.DeserializeObject<WebViewMessage>(args.WebMessageAsJson); var change = JsonSerializer.Deserialize<WebViewMessage>(args.WebMessageAsJson);
if (change.Type == "bold") if (change.Type == "bold")
{ {

View File

@@ -1,25 +0,0 @@
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Wino.Helpers
{
public static class JsonHelpers
{
public static async Task<T> ToObjectAsync<T>(string value)
{
return await Task.Run<T>(() =>
{
return JsonConvert.DeserializeObject<T>(value);
});
}
public static async Task<string> StringifyAsync(object value)
{
return await Task.Run<string>(() =>
{
return JsonConvert.SerializeObject(value);
});
}
}
}

View File

@@ -1,119 +0,0 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Wino.Helpers
{
// Use these extension methods to store and retrieve local and roaming app data
// More details regarding storing and retrieving app data at https://docs.microsoft.com/windows/uwp/app-settings/store-and-retrieve-app-data
public static class SettingsStorageExtensions
{
private const string FileExtension = ".json";
public static bool IsRoamingStorageAvailable(this ApplicationData appData)
{
return appData.RoamingStorageQuota == 0;
}
public static async Task SaveAsync<T>(this StorageFolder folder, string name, T content)
{
var file = await folder.CreateFileAsync(GetFileName(name), CreationCollisionOption.ReplaceExisting);
var fileContent = await JsonHelpers.StringifyAsync(content);
await FileIO.WriteTextAsync(file, fileContent);
}
public static async Task<T> ReadAsync<T>(this StorageFolder folder, string name)
{
if (!File.Exists(Path.Combine(folder.Path, GetFileName(name))))
{
return default;
}
var file = await folder.GetFileAsync($"{name}.json");
var fileContent = await FileIO.ReadTextAsync(file);
return await JsonHelpers.ToObjectAsync<T>(fileContent);
}
public static async Task SaveAsync<T>(this ApplicationDataContainer settings, string key, T value)
{
settings.SaveString(key, await JsonHelpers.StringifyAsync(value));
}
public static void SaveString(this ApplicationDataContainer settings, string key, string value)
{
settings.Values[key] = value;
}
public static async Task<T> ReadAsync<T>(this ApplicationDataContainer settings, string key)
{
object obj = null;
if (settings.Values.TryGetValue(key, out obj))
{
return await JsonHelpers.ToObjectAsync<T>((string)obj);
}
return default;
}
public static async Task<StorageFile> SaveFileAsync(this StorageFolder folder, byte[] content, string fileName, CreationCollisionOption options = CreationCollisionOption.ReplaceExisting)
{
if (content == null)
{
throw new ArgumentNullException(nameof(content));
}
if (string.IsNullOrEmpty(fileName))
{
throw new ArgumentException("File name is null or empty. Specify a valid file name", nameof(fileName));
}
var storageFile = await folder.CreateFileAsync(fileName, options);
await FileIO.WriteBytesAsync(storageFile, content);
return storageFile;
}
public static async Task<byte[]> ReadFileAsync(this StorageFolder folder, string fileName)
{
var item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
if ((item != null) && item.IsOfType(StorageItemTypes.File))
{
var storageFile = await folder.GetFileAsync(fileName);
byte[] content = await storageFile.ReadBytesAsync();
return content;
}
return null;
}
public static async Task<byte[]> ReadBytesAsync(this StorageFile file)
{
if (file != null)
{
using (IRandomAccessStream stream = await file.OpenReadAsync())
{
using (var reader = new DataReader(stream.GetInputStreamAt(0)))
{
await reader.LoadAsync((uint)stream.Size);
var bytes = new byte[stream.Size];
reader.ReadBytes(bytes);
return bytes;
}
}
}
return null;
}
private static string GetFileName(string name)
{
return string.Concat(name, FileExtension);
}
}
}

View File

@@ -48,7 +48,7 @@ function initializeJodit(fonts, defaultComposerFont, defaultComposerFontSize, de
const reader = new FileReader(); const reader = new FileReader();
reader.onload = function (event) { reader.onload = function (event) {
const base64Image = event.target.result; const base64Image = event.target.result;
insertImages([base64Image]); insertImages([{ data: base64Image, name: file.name }]);
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
@@ -121,8 +121,8 @@ function toggleToolbar(enable) {
} }
} }
function insertImages(images) { function insertImages(imagesInfo) {
images.forEach(image => { imagesInfo.forEach(imageInfo => {
editor.selection.insertHTML(`<img src="${image}" alt="Embedded Image">`); editor.selection.insertHTML(`<img src="${imageInfo.data}" alt="${imageInfo.name}">`);
}); });
}; };

View File

@@ -50,20 +50,13 @@
Description="Mail client designed for Windows 11" Description="Mail client designed for Windows 11"
BackgroundColor="transparent"> BackgroundColor="transparent">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" Square71x71Logo="Assets\SmallTile.png" Square310x310Logo="Assets\LargeTile.png"/> <uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" Square71x71Logo="Assets\SmallTile.png" Square310x310Logo="Assets\LargeTile.png"/>
<uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="transparent"/> <uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="transparent" uap5:Optional="true" />
<uap:LockScreen BadgeLogo="Assets\BadgeLogo.png" Notification="badgeAndTileText"/> <uap:LockScreen BadgeLogo="Assets\BadgeLogo.png" Notification="badgeAndTileText"/>
</uap:VisualElements> </uap:VisualElements>
<Extensions> <Extensions>
<!-- App updated task. Notifies about new version after each Store update. --> <!-- App updated task. Notifies about new version after each Store update. -->
<Extension Category="windows.updateTask" EntryPoint="Wino.BackgroundTasks.AppUpdatedTask" /> <Extension Category="windows.updateTask" EntryPoint="Wino.BackgroundTasks.AppUpdatedTask" />
<!-- SessionConnected task for background synchronization on startup. -->
<Extension Category="windows.backgroundTasks" EntryPoint="Wino.BackgroundTasks.SessionConnectedTask">
<BackgroundTasks>
<Task Type="systemEvent" />
</BackgroundTasks>
</Extension>
<!-- Protocol activation: mailto --> <!-- Protocol activation: mailto -->
<uap:Extension Category="windows.protocol"> <uap:Extension Category="windows.protocol">
<uap:Protocol Name="mailto" /> <uap:Protocol Name="mailto" />

View File

@@ -1,11 +0,0 @@
using System.Collections.Specialized;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.UWP.Services
{
public class LaunchProtocolService : ILaunchProtocolService
{
public object LaunchParameter { get; set; }
public NameValueCollection MailtoParameters { get; set; }
}
}

View File

@@ -493,7 +493,7 @@
VerticalAlignment="Center" VerticalAlignment="Center"
Click="ShowCCBCCClicked" Click="ShowCCBCCClicked"
GotFocus="CCBBCGotFocus" GotFocus="CCBBCGotFocus"
Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}"> Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.IsCCBCCVisible), Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="6"> <StackPanel Orientation="Horizontal" Spacing="6">
<PathIcon <PathIcon
HorizontalAlignment="Center" HorizontalAlignment="Center"
@@ -512,13 +512,14 @@
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="Cc: " Text="Cc: "
Visibility="{x:Bind helpers:XamlHelpers.ReverseVisibilityConverter(CCBCCShowButton.Visibility), Mode=OneWay}" /> Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}" />
<controls1:TokenizingTextBox <controls1:TokenizingTextBox
x:Name="CCBox" x:Name="CCBox"
Grid.Row="2" Grid.Row="2"
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center" VerticalAlignment="Center"
ItemsSource="{x:Bind ViewModel.CCItems, Mode=OneTime}"
LostFocus="AddressBoxLostFocus" LostFocus="AddressBoxLostFocus"
PlaceholderText="{x:Bind domain:Translator.ComposerToPlaceholder}" PlaceholderText="{x:Bind domain:Translator.ComposerToPlaceholder}"
SuggestedItemTemplate="{StaticResource SuggestionBoxTemplate}" SuggestedItemTemplate="{StaticResource SuggestionBoxTemplate}"
@@ -526,7 +527,7 @@
TokenDelimiter=";" TokenDelimiter=";"
TokenItemAdding="TokenItemAdding" TokenItemAdding="TokenItemAdding"
TokenItemTemplate="{StaticResource TokenBoxTemplate}" TokenItemTemplate="{StaticResource TokenBoxTemplate}"
Visibility="{x:Bind helpers:XamlHelpers.ReverseVisibilityConverter(CCBCCShowButton.Visibility), Mode=OneWay}" /> Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}" />
<TextBlock <TextBlock
x:Name="BccTextBlock" x:Name="BccTextBlock"
@@ -534,13 +535,14 @@
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="Bcc: " Text="Bcc: "
Visibility="{x:Bind helpers:XamlHelpers.ReverseVisibilityConverter(CCBCCShowButton.Visibility), Mode=OneWay}" /> Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}" />
<controls1:TokenizingTextBox <controls1:TokenizingTextBox
x:Name="BccBox" x:Name="BccBox"
Grid.Row="3" Grid.Row="3"
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center" VerticalAlignment="Center"
ItemsSource="{x:Bind ViewModel.BCCItems, Mode=OneTime}"
LostFocus="AddressBoxLostFocus" LostFocus="AddressBoxLostFocus"
PlaceholderText="{x:Bind domain:Translator.ComposerToPlaceholder}" PlaceholderText="{x:Bind domain:Translator.ComposerToPlaceholder}"
SuggestedItemTemplate="{StaticResource SuggestionBoxTemplate}" SuggestedItemTemplate="{StaticResource SuggestionBoxTemplate}"
@@ -548,7 +550,7 @@
TokenDelimiter=";" TokenDelimiter=";"
TokenItemAdding="TokenItemAdding" TokenItemAdding="TokenItemAdding"
TokenItemTemplate="{StaticResource TokenBoxTemplate}" TokenItemTemplate="{StaticResource TokenBoxTemplate}"
Visibility="{x:Bind helpers:XamlHelpers.ReverseVisibilityConverter(CCBCCShowButton.Visibility), Mode=OneWay}" /> Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}" />
<!-- Subject --> <!-- Subject -->
<TextBlock <TextBlock

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
@@ -13,7 +14,6 @@ using Microsoft.Toolkit.Uwp.Helpers;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using MimeKit; using MimeKit;
using Newtonsoft.Json;
using Windows.ApplicationModel.DataTransfer; using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation; using Windows.Foundation;
using Windows.Storage; using Windows.Storage;
@@ -167,7 +167,7 @@ namespace Wino.Views
foreach (var file in files) foreach (var file in files)
{ {
if (ValidateImageFile(file)) if (IsValidImageFile(file))
{ {
isValid = true; isValid = true;
} }
@@ -200,15 +200,21 @@ namespace Wino.Views
var storageItems = await e.DataView.GetStorageItemsAsync(); var storageItems = await e.DataView.GetStorageItemsAsync();
var files = storageItems.OfType<StorageFile>(); var files = storageItems.OfType<StorageFile>();
var imageDataURLs = new List<string>(); var imagesInformation = new List<ImageInfo>();
foreach (var file in files) foreach (var file in files)
{ {
if (ValidateImageFile(file)) if (IsValidImageFile(file))
imageDataURLs.Add(await GetDataURL(file)); {
imagesInformation.Add(new ImageInfo
{
Data = await GetDataURL(file),
Name = file.Name
});
}
} }
await InvokeScriptSafeAsync($"insertImages({JsonConvert.SerializeObject(imageDataURLs)});"); await InvokeScriptSafeAsync($"insertImages({JsonSerializer.Serialize(imagesInformation)});");
} }
} }
// State should be reset even when an exception occurs, otherwise the UI will be stuck in a dragging state. // State should be reset even when an exception occurs, otherwise the UI will be stuck in a dragging state.
@@ -240,7 +246,7 @@ namespace Wino.Views
} }
} }
private bool ValidateImageFile(StorageFile file) private bool IsValidImageFile(StorageFile file)
{ {
string[] allowedTypes = new string[] { ".jpg", ".jpeg", ".png" }; string[] allowedTypes = new string[] { ".jpg", ".jpeg", ".png" };
var fileType = file.FileType.ToLower(); var fileType = file.FileType.ToLower();
@@ -321,7 +327,7 @@ namespace Wino.Views
string script = functionName + "("; string script = functionName + "(";
for (int i = 0; i < parameters.Length; i++) for (int i = 0; i < parameters.Length; i++)
{ {
script += JsonConvert.SerializeObject(parameters[i]); script += JsonSerializer.Serialize(parameters[i]);
if (i < parameters.Length - 1) if (i < parameters.Length - 1)
{ {
script += ", "; script += ", ";
@@ -463,7 +469,7 @@ namespace Wino.Views
{ {
var editorContent = await InvokeScriptSafeAsync("GetHTMLContent();"); var editorContent = await InvokeScriptSafeAsync("GetHTMLContent();");
return JsonConvert.DeserializeObject<string>(editorContent); return JsonSerializer.Deserialize<string>(editorContent);
}); });
var underlyingThemeService = App.Current.Services.GetService<IUnderlyingThemeService>(); var underlyingThemeService = App.Current.Services.GetService<IUnderlyingThemeService>();
@@ -487,7 +493,7 @@ namespace Wino.Views
private void ScriptMessageReceived(CoreWebView2 sender, CoreWebView2WebMessageReceivedEventArgs args) private void ScriptMessageReceived(CoreWebView2 sender, CoreWebView2WebMessageReceivedEventArgs args)
{ {
var change = JsonConvert.DeserializeObject<WebViewMessage>(args.WebMessageAsJson); var change = JsonSerializer.Deserialize<WebViewMessage>(args.WebMessageAsJson);
if (change.Type == "bold") if (change.Type == "bold")
{ {
@@ -562,12 +568,7 @@ namespace Wino.Views
private void ShowCCBCCClicked(object sender, RoutedEventArgs e) private void ShowCCBCCClicked(object sender, RoutedEventArgs e)
{ {
CCBCCShowButton.Visibility = Visibility.Collapsed; ViewModel.IsCCBCCVisible = true;
CCTextBlock.Visibility = Visibility.Visible;
CCBox.Visibility = Visibility.Visible;
BccTextBlock.Visibility = Visibility.Visible;
BccBox.Visibility = Visibility.Visible;
} }
private async void TokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args) private async void TokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args)
@@ -591,7 +592,7 @@ namespace Wino.Views
if (boxTag == "ToBox") if (boxTag == "ToBox")
addedItem = await ViewModel.GetAddressInformationAsync(args.TokenText, ViewModel.ToItems); addedItem = await ViewModel.GetAddressInformationAsync(args.TokenText, ViewModel.ToItems);
else if (boxTag == "CCBox") else if (boxTag == "CCBox")
addedItem = await ViewModel.GetAddressInformationAsync(args.TokenText, ViewModel.CCItemsItems); addedItem = await ViewModel.GetAddressInformationAsync(args.TokenText, ViewModel.CCItems);
else if (boxTag == "BCCBox") else if (boxTag == "BCCBox")
addedItem = await ViewModel.GetAddressInformationAsync(args.TokenText, ViewModel.BCCItems); addedItem = await ViewModel.GetAddressInformationAsync(args.TokenText, ViewModel.BCCItems);
@@ -660,7 +661,7 @@ namespace Wino.Views
if (boxTag == "ToBox") if (boxTag == "ToBox")
addressCollection = ViewModel.ToItems; addressCollection = ViewModel.ToItems;
else if (boxTag == "CCBox") else if (boxTag == "CCBox")
addressCollection = ViewModel.CCItemsItems; addressCollection = ViewModel.CCItems;
else if (boxTag == "BCCBox") else if (boxTag == "BCCBox")
addressCollection = ViewModel.BCCItems; addressCollection = ViewModel.BCCItems;

View File

@@ -1,12 +1,12 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.AppCenter.Crashes; using Microsoft.AppCenter.Crashes;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Newtonsoft.Json;
using Windows.System; using Windows.System;
using Windows.UI.Xaml; using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
@@ -70,7 +70,7 @@ namespace Wino.Views
string script = functionName + "("; string script = functionName + "(";
for (int i = 0; i < parameters.Length; i++) for (int i = 0; i < parameters.Length; i++)
{ {
script += JsonConvert.SerializeObject(parameters[i]); script += JsonSerializer.Serialize(parameters[i]);
if (i < parameters.Length - 1) if (i < parameters.Length - 1)
{ {
script += ", "; script += ", ";
@@ -174,7 +174,7 @@ namespace Wino.Views
// We don't have shell initialized here. It's only standalone EML viewing. // We don't have shell initialized here. It's only standalone EML viewing.
// Shift command bar from top to adjust the design. // Shift command bar from top to adjust the design.
if (ViewModel.StatePersistanceService.ShouldShiftMailRenderingDesign) if (ViewModel.StatePersistenceService.ShouldShiftMailRenderingDesign)
RendererGridFrame.Margin = new Thickness(0, 24, 0, 0); RendererGridFrame.Margin = new Thickness(0, 24, 0, 0);
else else
RendererGridFrame.Margin = new Thickness(0, 0, 0, 0); RendererGridFrame.Margin = new Thickness(0, 0, 0, 0);

View File

@@ -298,14 +298,12 @@
<Compile Include="Dialogs\AccountCreationDialog.xaml.cs"> <Compile Include="Dialogs\AccountCreationDialog.xaml.cs">
<DependentUpon>AccountCreationDialog.xaml</DependentUpon> <DependentUpon>AccountCreationDialog.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Helpers\JsonHelpers.cs" />
<Compile Include="Extensions\AnimationExtensions.cs" /> <Compile Include="Extensions\AnimationExtensions.cs" />
<Compile Include="Extensions\CompositionExtensions.Implicit.cs" /> <Compile Include="Extensions\CompositionExtensions.Implicit.cs" />
<Compile Include="Extensions\CompositionExtensions.Size.cs" /> <Compile Include="Extensions\CompositionExtensions.Size.cs" />
<Compile Include="Extensions\CompositionEnums.cs" /> <Compile Include="Extensions\CompositionEnums.cs" />
<Compile Include="Extensions\EnumerableExtensions.cs" /> <Compile Include="Extensions\EnumerableExtensions.cs" />
<Compile Include="Extensions\UtilExtensions.cs" /> <Compile Include="Extensions\UtilExtensions.cs" />
<Compile Include="Helpers\SettingsStorageExtensions.cs" />
<Compile Include="MenuFlyouts\FilterMenuFlyout.cs" /> <Compile Include="MenuFlyouts\FilterMenuFlyout.cs" />
<Compile Include="Controls\ImagePreviewControl.cs" /> <Compile Include="Controls\ImagePreviewControl.cs" />
<Compile Include="Controls\MailItemDisplayInformationControl.xaml.cs"> <Compile Include="Controls\MailItemDisplayInformationControl.xaml.cs">
@@ -335,7 +333,6 @@
<Compile Include="Selectors\RendererCommandBarItemTemplateSelector.cs" /> <Compile Include="Selectors\RendererCommandBarItemTemplateSelector.cs" />
<Compile Include="Services\ApplicationResourceManager.cs" /> <Compile Include="Services\ApplicationResourceManager.cs" />
<Compile Include="Services\DialogService.cs" /> <Compile Include="Services\DialogService.cs" />
<Compile Include="Services\LaunchProtocolService.cs" />
<Compile Include="Services\WinoNavigationService.cs" /> <Compile Include="Services\WinoNavigationService.cs" />
<Compile Include="Styles\CommandBarItems.xaml.cs"> <Compile Include="Styles\CommandBarItems.xaml.cs">
<DependentUpon>CommandBarItems.xaml</DependentUpon> <DependentUpon>CommandBarItems.xaml</DependentUpon>
@@ -892,4 +889,4 @@
<Target Name="AfterBuild"> <Target Name="AfterBuild">
</Target> </Target>
--> -->
</Project> </Project>

View File

@@ -3,5 +3,5 @@
/// <summary> /// <summary>
/// When client established a healthy connection to the server. /// When client established a healthy connection to the server.
/// </summary> /// </summary>
public record WinoServerConnectionEstrablished; public record WinoServerConnectionEstablished;
} }

View File

@@ -11,7 +11,7 @@
<Identity <Identity
Name="58272BurakKSE.WinoMailPreview" Name="58272BurakKSE.WinoMailPreview"
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911" Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
Version="1.8.0.0" /> Version="1.8.1.0" />
<Extensions> <Extensions>
<!-- Publisher Cache Folders --> <!-- Publisher Cache Folders -->
@@ -48,20 +48,13 @@
Square150x150Logo="Images\Square150x150Logo.png" Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png"> Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/> <uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/>
<uap:SplashScreen Image="Images\SplashScreen.png" /> <uap:SplashScreen Image="Images\SplashScreen.png" uap5:Optional="true" />
</uap:VisualElements> </uap:VisualElements>
<Extensions> <Extensions>
<!-- App updated task. Notifies about new version after each Store update. --> <!-- App updated task. Notifies about new version after each Store update. -->
<Extension Category="windows.updateTask" EntryPoint="Wino.BackgroundTasks.AppUpdatedTask" /> <Extension Category="windows.updateTask" EntryPoint="Wino.BackgroundTasks.AppUpdatedTask" />
<!-- SessionConnected task for background synchronization on startup. -->
<Extension Category="windows.backgroundTasks" EntryPoint="Wino.BackgroundTasks.SessionConnectedTask">
<BackgroundTasks>
<Task Type="systemEvent" />
</BackgroundTasks>
</Extension>
<!-- Protocol activation: mailto --> <!-- Protocol activation: mailto -->
<uap:Extension Category="windows.protocol"> <uap:Extension Category="windows.protocol">
<uap:Protocol Name="mailto" /> <uap:Protocol Name="mailto" />

View File

@@ -173,7 +173,7 @@ namespace Wino.Server
if (status != AppServiceConnectionStatus.Success) if (status != AppServiceConnectionStatus.Success)
{ {
// TODO: Handle connection error Log.Error("Opening server connection failed. Status: {status}", status);
DisposeConnection(); DisposeConnection();
} }
@@ -219,13 +219,24 @@ namespace Wino.Server
{ MessageConstants.MessageDataTypeKey, message.GetType().Name } { MessageConstants.MessageDataTypeKey, message.GetType().Name }
}; };
await connection.SendMessageAsync(set); try
{
await connection.SendMessageAsync(set);
}
catch (InvalidOperationException)
{
// Connection might've been disposed during the SendMessageAsync call.
// This is a safe way to handle the exception.
// We don't lock the connection since this request may take sometime to complete.
}
catch (Exception exception)
{
Log.Error(exception, "SendMessageAsync threw an exception");
}
} }
private void OnConnectionClosed(AppServiceConnection sender, AppServiceClosedEventArgs args) private void OnConnectionClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
{ {
// TODO: Handle connection closed.
// UWP app might've been terminated or suspended. // UWP app might've been terminated or suspended.
// At this point, we must keep active synchronizations going, but connection is lost. // 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. // As long as this process is alive, database will be kept updated, but no messages will be sent.