58 Commits

Author SHA1 Message Date
Burak Kaan Köse
d623129d56 Merge pull request #325 from bkaankose/features/mail-list-splitter
Mail List splitter
2024-08-19 21:21:28 +02:00
Aleh Khantsevich
9cc4c33bb1 Fix multiselect and hide sizer 2024-08-19 21:16:58 +02:00
Burak Kaan Köse
c087b40d4a Renaming event handlers and fixing the search box margin issue. 2024-08-19 21:07:21 +02:00
Aleh Khantsevich
a82e074bd4 Fix merge conflicts 2024-08-19 20:50:14 +02:00
Burak Kaan Köse
3365c099bb Missing comments. 2024-08-19 20:43:26 +02:00
Aleh Khantsevich
d8705de26f Replaced setting with reset button 2024-08-19 20:41:55 +02:00
Burak Kaan Köse
3af181e736 Merge pull request #324 from bkaankose/feature/Aliases
E-mail Aliases Support
2024-08-19 20:37:58 +02:00
Burak Kaan Köse
ba6c01b7c6 Missing comments. 2024-08-19 19:16:54 +02:00
Burak Kaan Köse
7a7cdcb041 Remove json.net dependency to STJson 2024-08-19 19:16:47 +02:00
Burak Kaan Köse
09e52bf199 Remove not implemented comment. 2024-08-19 19:05:54 +02:00
Burak Kaan Köse
a8c39a1587 Fixing send draft issue with OutlookSynchronizer. 2024-08-19 19:02:33 +02:00
Aleh Khantsevich
68536d6c34 Fix padding in narrow state 2024-08-19 18:49:35 +02:00
Aleh Khantsevich
f57c27e755 Fix multiple items selected 2024-08-19 17:15:59 +02:00
Aleh Khantsevich
9a97a27c8a Init 2024-08-19 16:26:15 +02:00
Burak Kaan Köse
07bb90dda9 Refactoring outlook draft creation and sending. 2024-08-19 03:44:16 +02:00
Burak Kaan Köse
3bb156f4da Handling of OutlookSynchronizer alias. 2024-08-18 22:45:23 +02:00
Burak Kaan Köse
e13e0efcc6 Bump some nugets. 2024-08-18 22:44:55 +02:00
Burak Kaan Köse
3ae0a94159 Remove folder init progress bar in mail list page. 2024-08-18 22:27:31 +02:00
Burak Kaan Köse
eec67ec7dc Fixed an issue where re-loading messages with attachments break the included attachment encodings. 2024-08-18 22:25:29 +02:00
Burak Kaan Köse
cf51853eec Removed non-mandatory reply-to validation. 2024-08-18 01:11:23 +02:00
Burak Kaan Köse
67838b28a4 Syntactic sugar. 2024-08-18 01:06:21 +02:00
Burak Kaan Köse
bf68e3b7d5 Fix sending draft issue. 2024-08-18 01:05:43 +02:00
Burak Kaan Köse
91ed0bb8bd Ability to select alias in composer page. 2024-08-17 22:55:58 +02:00
Burak Kaan Köse
55fe791c2a Handling of missing mime downloads and mail processed messages. 2024-08-17 20:19:01 +02:00
Burak Kaan Köse
747efac2ec Reworked aliases. 2024-08-17 19:54:52 +02:00
Burak Kaan Köse
a87df2e9f6 Fixed an issue where deleting account navigates back to mail list of the next account. 2024-08-17 19:54:44 +02:00
Burak Kaan Köse
2e4a664744 More detailed ImapImplementation for ID extension. 2024-08-17 19:53:50 +02:00
Tiktack
579a22ea45 Remove self from reply all when no other recepients (#319) 2024-08-17 15:00:25 +02:00
Burak Kaan Köse
abff850427 Managing account aliases and profile synchronization for outlook and gmail. 2024-08-17 03:43:37 +02:00
Burak Kaan Köse
f1154058ba Fix ascending download for messages. QQ server issue will be handled later. 2024-08-17 00:03:45 +02:00
Burak Kaan Köse
cf9f308b7f Updating aliases during profile sync for Gmail. 2024-08-16 01:29:31 +02:00
Burak Kaan Köse
1791df236c Remove unused extension class. 2024-08-16 01:03:00 +02:00
Burak Kaan Köse
7211f94f08 Try - catch for outlook profile sync. 2024-08-16 00:40:10 +02:00
Burak Kaan Köse
7b0343c87f Added sender name comment for gmail. 2024-08-16 00:37:50 +02:00
Burak Kaan Köse
b80f0276b4 Sender Name and Profile Picture synchronization for Outlook 2024-08-16 00:37:38 +02:00
Burak Kaan Köse
8f66fcbb00 Activated contact service for Gmail to retrieve profile picture and sender name. 2024-08-15 23:57:45 +02:00
Burak Kaan Köse
fe449ee1f3 Comments for alias entity. 2024-08-15 16:13:18 +02:00
Burak Kaan Köse
34d6d95186 Including ReplyToAddress for alias. 2024-08-15 16:11:12 +02:00
Burak Kaan Köse
05ddc0660a Creating MailAccountAlias entity. 2024-08-15 16:02:02 +02:00
Burak Kaan Köse
c6047a8428 Version bump 2024-08-13 23:40:09 +02:00
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
122 changed files with 2848 additions and 1666 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

@@ -44,6 +44,11 @@ namespace Wino.Core.Domain.Entities
/// </summary> /// </summary>
public string AccountColorHex { get; set; } public string AccountColorHex { get; set; }
/// <summary>
/// Base64 encoded profile picture of the account.
/// </summary>
public string Base64ProfilePictureData { get; set; }
/// <summary> /// <summary>
/// Gets or sets the listing order of the account in the accounts list. /// Gets or sets the listing order of the account in the accounts list.
/// </summary> /// </summary>
@@ -78,5 +83,15 @@ namespace Wino.Core.Domain.Entities
/// </summary> /// </summary>
[Ignore] [Ignore]
public MailAccountPreferences Preferences { get; set; } public MailAccountPreferences Preferences { get; set; }
/// <summary>
/// Gets whether the account can perform ProfileInformation sync type.
/// </summary>
public bool IsProfileInfoSyncSupported => ProviderType == MailProviderType.Outlook || ProviderType == MailProviderType.Office365 || ProviderType == MailProviderType.Gmail;
/// <summary>
/// Gets whether the account can perform AliasInformation sync type.
/// </summary>
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail;
} }
} }

View File

@@ -0,0 +1,56 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities
{
public class RemoteAccountAlias
{
/// <summary>
/// Display address of the alias.
/// </summary>
public string AliasAddress { get; set; }
/// <summary>
/// Address to be included in Reply-To header when alias is used for sending messages.
/// </summary>
public string ReplyToAddress { get; set; }
/// <summary>
/// Whether this alias is the primary alias for the account.
/// </summary>
public bool IsPrimary { get; set; }
/// <summary>
/// Whether the alias is verified by the server.
/// Only Gmail aliases are verified for now.
/// Non-verified alias messages might be rejected by SMTP server.
/// </summary>
public bool IsVerified { get; set; }
/// <summary>
/// Whether this alias is the root alias for the account.
/// Root alias means the first alias that was created for the account.
/// It can't be deleted or changed.
/// </summary>
public bool IsRootAlias { get; set; }
}
public class MailAccountAlias : RemoteAccountAlias
{
/// <summary>
/// Unique Id for the alias.
/// </summary>
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>
/// Account id that this alias is attached to.
/// </summary>
public Guid AccountId { get; set; }
/// <summary>
/// Root aliases can't be deleted.
/// </summary>
public bool CanDelete => !IsRootAlias;
}
}

View File

@@ -141,7 +141,7 @@ namespace Wino.Core.Domain.Entities
/// </summary> /// </summary>
[Ignore] [Ignore]
public MailAccount AssignedAccount { get; set; } public MailAccount AssignedAccount { get; set; }
public IEnumerable<Guid> GetContainingIds() => new[] { UniqueId }; public IEnumerable<Guid> GetContainingIds() => [UniqueId];
public override string ToString() => $"{Subject} <-> {Id}"; public override string ToString() => $"{Subject} <-> {Id}";
} }
} }

View File

@@ -9,6 +9,7 @@
ManuelSetupWaiting, ManuelSetupWaiting,
TestingConnection, TestingConnection,
AutoDiscoverySetup, AutoDiscoverySetup,
AutoDiscoveryInProgress AutoDiscoveryInProgress,
FetchingProfileInformation
} }
} }

View File

@@ -4,8 +4,10 @@
{ {
FoldersOnly, // Only synchronize folder metadata. FoldersOnly, // Only synchronize folder metadata.
ExecuteRequests, // Run the queued requests, and then synchronize if needed. ExecuteRequests, // Run the queued requests, and then synchronize if needed.
Inbox, // Only Inbox Inbox, // Only Inbox, Sent and Draft folders.
Custom, // Only sync folders that are specified in the options. Custom, // Only sync folders that are specified in the options.
Full, // Synchronize everything Full, // Synchronize all folders. This won't update profile or alias information.
UpdateProfile, // Only update profile information
Alias, // Only update alias information
} }
} }

View File

@@ -23,5 +23,6 @@
LanguageTimePage, LanguageTimePage,
AppPreferencesPage, AppPreferencesPage,
SettingOptionsPage, SettingOptionsPage,
AliasManagementPage
} }
} }

View File

@@ -0,0 +1,7 @@
namespace Wino.Core.Domain.Exceptions
{
public class MissingAliasException : System.Exception
{
public MissingAliasException() : base(Translator.Exception_MissingAlias) { }
}
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Models.Accounts;
namespace Wino.Core.Domain.Interfaces namespace Wino.Core.Domain.Interfaces
{ {
@@ -100,5 +101,59 @@ namespace Wino.Core.Domain.Interfaces
/// </summary> /// </summary>
/// <param name="accountIdOrderPair">AccountId-OrderNumber pair for all accounts.</param> /// <param name="accountIdOrderPair">AccountId-OrderNumber pair for all accounts.</param>
Task UpdateAccountOrdersAsync(Dictionary<Guid, int> accountIdOrderPair); Task UpdateAccountOrdersAsync(Dictionary<Guid, int> accountIdOrderPair);
/// <summary>
/// Returns the account aliases.
/// </summary>
/// <param name="accountId">Account id.</param>
/// <returns>A list of MailAccountAlias that has e-mail aliases.</returns>
Task<List<MailAccountAlias>> GetAccountAliasesAsync(Guid accountId);
/// <summary>
/// Updated account's aliases.
/// </summary>
/// <param name="accountId">Account id to update aliases for.</param>
/// <param name="aliases">Full list of updated aliases.</param>
/// <returns></returns>
Task UpdateAccountAliasesAsync(Guid accountId, List<MailAccountAlias> aliases);
/// <summary>
/// Delete account alias.
/// </summary>
/// <param name="aliasId">Alias to remove.</param>
Task DeleteAccountAliasAsync(Guid aliasId);
/// <summary>
/// Updated profile information of the account.
/// </summary>
/// <param name="accountId">Account id to update info for.</param>
/// <param name="profileInformation">Info data.</param>
/// <returns></returns>
Task UpdateProfileInformationAsync(Guid accountId, ProfileInformation profileInformation);
/// <summary>
/// Creates a root + primary alias for the account.
/// This is only called when the account is created.
/// </summary>
/// <param name="accountId">Account id.</param>
/// <param name="address">Address to create root primary alias from.</param>
Task CreateRootAliasAsync(Guid accountId, string address);
/// <summary>
/// Will compare local-remote aliases and update the local ones or add/delete new ones.
/// </summary>
/// <param name="remoteAccountAliases">Remotely fetched basic alias info from synchronizer.</param>
/// <param name="account">Account to update remote aliases for..</param>
Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases);
/// <summary>
/// Gets the primary account alias for the given account id.
/// Used when creating draft messages.
/// </summary>
/// <param name="accountId">Account id.</param>
/// <returns>Primary alias for the account.</returns>
Task<MailAccountAlias> GetPrimaryAccountAliasAsync(Guid accountId);
} }
} }

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

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MailKit; using MailKit;
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.Accounts;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Domain.Models.Synchronization;
@@ -43,6 +44,13 @@ namespace Wino.Core.Domain.Interfaces
/// <returns>Result summary of synchronization.</returns> /// <returns>Result summary of synchronization.</returns>
Task<SynchronizationResult> SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default); Task<SynchronizationResult> SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Synchronizes profile information with the server.
/// Sender name and Profile picture are updated.
/// </summary>
/// <returns>Profile information model that holds the values.</returns>
Task<ProfileInformation> GetProfileInformationAsync();
/// <summary> /// <summary>
/// Downloads a single MIME message from the server and saves it to disk. /// Downloads a single MIME message from the server and saves it to disk.
/// </summary> /// </summary>

View File

@@ -0,0 +1,9 @@
using Wino.Core.Domain.Entities;
namespace Wino.Core.Domain.Interfaces
{
public interface ICreateAccountAliasDialog
{
public MailAccountAlias CreatedAccountAlias { get; set; }
}
}

View File

@@ -53,5 +53,6 @@ namespace Wino.Core.Domain.Interfaces
/// </summary> /// </summary>
/// <returns>Signature information. Null if canceled.</returns> /// <returns>Signature information. Null if canceled.</returns>
Task<AccountSignature> ShowSignatureEditorDialog(AccountSignature signatureModel = null); Task<AccountSignature> ShowSignatureEditorDialog(AccountSignature signatureModel = null);
Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync();
} }
} }

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.
/// </summary>
object LaunchParameter { get; set; } object LaunchParameter { get; set; }
NameValueCollection MailtoParameters { 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

@@ -2,5 +2,5 @@
namespace Wino.Core.Domain.Models.Accounts namespace Wino.Core.Domain.Models.Accounts
{ {
public record AccountCreationDialogResult(MailProviderType ProviderType, string AccountName, string SenderName, string AccountColorHex = ""); public record AccountCreationDialogResult(MailProviderType ProviderType, string AccountName, string AccountColorHex = "");
} }

View File

@@ -0,0 +1,9 @@
namespace Wino.Core.Domain.Models.Accounts
{
/// <summary>
/// Encapsulates the profile information of an account.
/// </summary>
/// <param name="SenderName">Display sender name for the account.</param>
/// <param name="Base64ProfilePictureData">Base 64 encoded profile picture data of the account. Thumbnail size.</param>
public record ProfileInformation(string SenderName, string Base64ProfilePictureData);
}

View File

@@ -14,7 +14,6 @@ namespace Wino.Core.Domain.Models.Accounts
public string ProviderImage => $"ms-appx:///Assets/Providers/{Type}.png"; public string ProviderImage => $"ms-appx:///Assets/Providers/{Type}.png";
public bool IsSupported => Type == MailProviderType.Outlook || Type == MailProviderType.Gmail || Type == MailProviderType.IMAP4; public bool IsSupported => Type == MailProviderType.Outlook || Type == MailProviderType.Gmail || Type == MailProviderType.IMAP4;
public bool RequireSenderNameOnCreationDialog => Type != MailProviderType.IMAP4;
public ProviderDetail(MailProviderType type) public ProviderDetail(MailProviderType type)
{ {

View File

@@ -33,7 +33,7 @@ namespace Wino.Core.Domain.Models.Authorization
ClientId = clientId; ClientId = clientId;
// Creates the OAuth 2.0 authorization request. // Creates the OAuth 2.0 authorization request.
return string.Format("{0}?response_type=code&scope=https://mail.google.com/ https://www.googleapis.com/auth/gmail.labels&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}", return string.Format("{0}?response_type=code&scope=https://mail.google.com/ https://www.googleapis.com/auth/gmail.labels https://www.googleapis.com/auth/userinfo.profile&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}",
authorizationEndpoint, authorizationEndpoint,
Uri.EscapeDataString(RedirectUri), Uri.EscapeDataString(RedirectUri),
ClientId, ClientId,

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
{
[JsonIgnore]
public MimeMessage ReferenceMimeMessage { get; set; }
public MailCopy ReferenceMailCopy { get; set; }
public DraftCreationReason Reason { 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,55 @@
using System;
using System.Text.Json.Serialization;
using MimeKit;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Extensions;
namespace Wino.Core.Domain.Models.MailItem;
public class DraftPreparationRequest
{
public DraftPreparationRequest(MailAccount account,
MailCopy createdLocalDraftCopy,
string base64EncodedMimeMessage,
DraftCreationReason reason,
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;
Reason = reason;
}
[JsonConstructor]
private DraftPreparationRequest() { }
public MailCopy CreatedLocalDraftCopy { get; set; }
public MailCopy ReferenceMailCopy { get; set; }
public string Base64LocalDraftMimeMessage { get; set; }
public DraftCreationReason Reason { get; set; }
[JsonIgnore]
private MimeMessage createdLocalDraftMimeMessage;
[JsonIgnore]
public MimeMessage CreatedLocalDraftMimeMessage
{
get
{
createdLocalDraftMimeMessage ??= Base64LocalDraftMimeMessage.GetMimeMessageFromBase64();
return createdLocalDraftMimeMessage;
}
}
public MailAccount Account { get; set; }
}

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

@@ -5,45 +5,17 @@ using Wino.Core.Domain.Extensions;
namespace Wino.Core.Domain.Models.MailItem namespace Wino.Core.Domain.Models.MailItem
{ {
public class SendDraftPreparationRequest public record SendDraftPreparationRequest(MailCopy MailItem,
MailAccountAlias SendingAlias,
MailItemFolder SentFolder,
MailItemFolder DraftFolder,
MailAccountPreferences AccountPreferences,
string Base64MimeMessage)
{ {
public MailCopy MailItem { get; set; }
public string Base64MimeMessage { get; set; }
public MailItemFolder SentFolder { get; set; }
public MailItemFolder DraftFolder { get; set; }
public MailAccountPreferences AccountPreferences { get; set; }
public SendDraftPreparationRequest(MailCopy mailItem,
MailItemFolder sentFolder,
MailItemFolder draftFolder,
MailAccountPreferences accountPreferences,
string base64MimeMessage)
{
MailItem = mailItem;
SentFolder = sentFolder;
DraftFolder = draftFolder;
AccountPreferences = accountPreferences;
Base64MimeMessage = base64MimeMessage;
}
[JsonConstructor]
private SendDraftPreparationRequest() { }
[JsonIgnore] [JsonIgnore]
private MimeMessage mime; private MimeMessage mime;
[JsonIgnore] [JsonIgnore]
public MimeMessage Mime public MimeMessage Mime => mime ??= Base64MimeMessage.GetMimeMessageFromBase64();
{
get
{
if (mime == null)
{
mime = Base64MimeMessage.GetMimeMessageFromBase64();
}
return mime;
}
}
} }
} }

View File

@@ -1,4 +0,0 @@
namespace Wino.Core.Domain.Models.Personalization
{
public record MailListPaneLengthPreferences(string Title, double Length);
}

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

@@ -3,14 +3,11 @@ using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Models.Requests namespace Wino.Core.Domain.Models.Requests
{ {
/// <summary> /// <summary>
/// Encapsulates request to queue and account for synchronizer. /// Encapsulates request to queue and account for synchronizer.
/// </summary> /// </summary>
/// <param name="AccountId"><inheritdoc/></param> /// <param name="AccountId">Which account to execute this request for.</param>
/// <param name="Request"></param>
/// <param name="Request">Prepared request for the server.</param> /// <param name="Request">Prepared request for the server.</param>
/// <param name="AccountId">Whihc account to execute this request for.</param>
public record ServerRequestPackage(Guid AccountId, IRequestBase Request) : IClientMessage public record ServerRequestPackage(Guid AccountId, IRequestBase Request) : IClientMessage
{ {
public override string ToString() => $"Server Package: {Request.GetType().Name}"; public override string ToString() => $"Server Package: {Request.GetType().Name}";

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

@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
namespace Wino.Core.Domain.Models.Synchronization namespace Wino.Core.Domain.Models.Synchronization
@@ -15,14 +16,23 @@ namespace Wino.Core.Domain.Models.Synchronization
/// It's ignored in serialization. Client should not react to this. /// It's ignored in serialization. Client should not react to this.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public IEnumerable<IMailItem> DownloadedMessages { get; set; } = new List<IMailItem>(); public IEnumerable<IMailItem> DownloadedMessages { get; set; } = [];
public ProfileInformation ProfileInformation { get; set; }
public SynchronizationCompletedState CompletedState { get; set; } public SynchronizationCompletedState CompletedState { get; set; }
public static SynchronizationResult Empty => new() { CompletedState = SynchronizationCompletedState.Success }; public static SynchronizationResult Empty => new() { CompletedState = SynchronizationCompletedState.Success };
public static SynchronizationResult Completed(IEnumerable<IMailItem> downloadedMessages) public static SynchronizationResult Completed(IEnumerable<IMailItem> downloadedMessages, ProfileInformation profileInformation = null)
=> new() { DownloadedMessages = downloadedMessages, CompletedState = SynchronizationCompletedState.Success }; => new()
{
DownloadedMessages = downloadedMessages,
ProfileInformation = profileInformation,
CompletedState = SynchronizationCompletedState.Success
};
public static SynchronizationResult Canceled => new() { CompletedState = SynchronizationCompletedState.Canceled }; public static SynchronizationResult Canceled => new() { CompletedState = SynchronizationCompletedState.Canceled };
public static SynchronizationResult Failed => new() { CompletedState = SynchronizationCompletedState.Failed };
} }
} }

View File

@@ -3,6 +3,7 @@
"AccountCreationDialog_Initializing": "initializing", "AccountCreationDialog_Initializing": "initializing",
"AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.", "AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.",
"AccountCreationDialog_SigninIn": "Account information is being saved.", "AccountCreationDialog_SigninIn": "Account information is being saved.",
"AccountCreationDialog_FetchingProfileInformation": "Fetching profile details.",
"AccountEditDialog_Message": "Account Name", "AccountEditDialog_Message": "Account Name",
"AccountEditDialog_Title": "Edit Account", "AccountEditDialog_Title": "Edit Account",
"AccountPickerDialog_Title": "Pick an account", "AccountPickerDialog_Title": "Pick an account",
@@ -21,6 +22,8 @@
"BasicIMAPSetupDialog_Password": "Password", "BasicIMAPSetupDialog_Password": "Password",
"BasicIMAPSetupDialog_Title": "IMAP Account", "BasicIMAPSetupDialog_Title": "IMAP Account",
"Buttons_AddAccount": "Add Account", "Buttons_AddAccount": "Add Account",
"Buttons_AddNewAlias": "Add New Alias",
"Buttons_SyncAliases": "Synchronize Aliases",
"Buttons_ApplyTheme": "Apply Theme", "Buttons_ApplyTheme": "Apply Theme",
"Buttons_Browse": "Browse", "Buttons_Browse": "Browse",
"Buttons_Cancel": "Cancel", "Buttons_Cancel": "Cancel",
@@ -42,6 +45,7 @@
"Buttons_SignIn": "Sign In", "Buttons_SignIn": "Sign In",
"Buttons_TryAgain": "Try Again", "Buttons_TryAgain": "Try Again",
"Buttons_Yes": "Yes", "Buttons_Yes": "Yes",
"Buttons_Reset": "Reset",
"Center": "Center", "Center": "Center",
"ComingSoon": "Coming soon...", "ComingSoon": "Coming soon...",
"ComposerFrom": "From: ", "ComposerFrom": "From: ",
@@ -64,6 +68,16 @@
"CustomThemeBuilder_WallpaperTitle": "Set custom wallpaper", "CustomThemeBuilder_WallpaperTitle": "Set custom wallpaper",
"DialogMessage_AccountLimitMessage": "You have reached the account creation limit.\nWould you like to purchase 'Unlimited Account' add-on to continue?", "DialogMessage_AccountLimitMessage": "You have reached the account creation limit.\nWould you like to purchase 'Unlimited Account' add-on to continue?",
"DialogMessage_AccountLimitTitle": "Account Limit Reached", "DialogMessage_AccountLimitTitle": "Account Limit Reached",
"DialogMessage_AliasNotSelectedTitle": "Missing Alias",
"DialogMessage_AliasNotSelectedMessage": "You must select an alias before sending a message.",
"DialogMessage_AliasExistsTitle": "Existing Alias",
"DialogMessage_AliasExistsMessage": "This alias is already in use.",
"DialogMessage_InvalidAliasTitle": "Invalid Alias",
"DialogMessage_InvalidAliasMessage": "This alias is not valid. Make sure all addresses of the alias are valid e-mail addresses.",
"DialogMessage_CantDeleteRootAliasTitle": "Can't Delete Alias",
"DialogMessage_CantDeleteRootAliasMessage": "Root alias can't be deleted. This is your main identity associated with your account setup.",
"DialogMessage_AliasCreatedTitle": "Created New Alias",
"DialogMessage_AliasCreatedMessage": "New alias is succesfully created.",
"DialogMessage_CleanupFolderMessage": "Do you want to permanently delete all the mails in this folder?", "DialogMessage_CleanupFolderMessage": "Do you want to permanently delete all the mails in this folder?",
"DialogMessage_CleanupFolderTitle": "Cleanup Folder", "DialogMessage_CleanupFolderTitle": "Cleanup Folder",
"DialogMessage_ComposerMissingRecipientMessage": "Message has no recipient.", "DialogMessage_ComposerMissingRecipientMessage": "Message has no recipient.",
@@ -92,9 +106,16 @@
"DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton": "Go to website", "DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton": "Go to website",
"DialogMessage_UnsubscribeConfirmationMailtoMessage": "Do you want to stop getting messages from {0}? Wino will unsubscribe for you by sending an email from your email account to {1}.", "DialogMessage_UnsubscribeConfirmationMailtoMessage": "Do you want to stop getting messages from {0}? Wino will unsubscribe for you by sending an email from your email account to {1}.",
"Dialog_DontAskAgain": "Don't ask again", "Dialog_DontAskAgain": "Don't ask again",
"CreateAccountAliasDialog_Title": "Create Account Alias",
"CreateAccountAliasDialog_Description": "Make sure your outgoing server allows sending mails from this alias.",
"CreateAccountAliasDialog_AliasAddress": "Address",
"CreateAccountAliasDialog_AliasAddressPlaceholder": "eg. support@mydomain.com",
"CreateAccountAliasDialog_ReplyToAddress": "Reply-To Address",
"CreateAccountAliasDialog_ReplyToAddressPlaceholder": "admin@mydomain.com",
"DiscordChannelDisclaimerMessage": "Wino doesn't have it's own Discord server, but special 'wino-mail' channel is hosted at 'Developer Sanctuary' server.\nTo get the updates about Wino please join Developer Sanctuary server and follow 'wino-mail' channel under 'Community Projects'\n\nYou will be directed to server URL since Discord doesn't support channel invites.", "DiscordChannelDisclaimerMessage": "Wino doesn't have it's own Discord server, but special 'wino-mail' channel is hosted at 'Developer Sanctuary' server.\nTo get the updates about Wino please join Developer Sanctuary server and follow 'wino-mail' channel under 'Community Projects'\n\nYou will be directed to server URL since Discord doesn't support channel invites.",
"DiscordChannelDisclaimerTitle": "Important Discord Information", "DiscordChannelDisclaimerTitle": "Important Discord Information",
"Draft": "Draft", "Draft": "Draft",
"Busy": "Busy",
"EditorToolbarOption_Draw": "Draw", "EditorToolbarOption_Draw": "Draw",
"EditorToolbarOption_Format": "Format", "EditorToolbarOption_Format": "Format",
"EditorToolbarOption_Insert": "Insert", "EditorToolbarOption_Insert": "Insert",
@@ -106,6 +127,7 @@
"ElementTheme_Light": "Light mode", "ElementTheme_Light": "Light mode",
"Emoji": "Emoji", "Emoji": "Emoji",
"Exception_WinoServerException": "Wino server failed.", "Exception_WinoServerException": "Wino server failed.",
"Exception_MailProcessing": "This mail is still being processed. Please try again after few seconds.",
"Exception_ImapAutoDiscoveryFailed": "Couldn't find mailbox settings.", "Exception_ImapAutoDiscoveryFailed": "Couldn't find mailbox settings.",
"Exception_ImapClientPoolFailed": "IMAP Client Pool failed.", "Exception_ImapClientPoolFailed": "IMAP Client Pool failed.",
"Exception_AuthenticationCanceled": "Authentication canceled", "Exception_AuthenticationCanceled": "Authentication canceled",
@@ -113,6 +135,9 @@
"Exception_CustomThemeMissingName": "You must provide a name.", "Exception_CustomThemeMissingName": "You must provide a name.",
"Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.", "Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.",
"Exception_FailedToSynchronizeFolders": "Failed to synchronize folders", "Exception_FailedToSynchronizeFolders": "Failed to synchronize folders",
"Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases",
"Exception_MissingAlias": "Primary alias does not exist for this account. Creating draft failed.",
"Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information",
"Exception_GoogleAuthCallbackNull": "Callback uri is null on activation.", "Exception_GoogleAuthCallbackNull": "Callback uri is null on activation.",
"Exception_GoogleAuthCorruptedCode": "Corrupted authorization response.", "Exception_GoogleAuthCorruptedCode": "Corrupted authorization response.",
"Exception_GoogleAuthError": "OAuth authorization error: {0}", "Exception_GoogleAuthError": "OAuth authorization error: {0}",
@@ -253,6 +278,8 @@
"Info_UnsubscribeLinkInvalidMessage": "This unsubscribe link is invalid. Failed to unsubscribe from the list.", "Info_UnsubscribeLinkInvalidMessage": "This unsubscribe link is invalid. Failed to unsubscribe from the list.",
"Info_UnsubscribeSuccessMessage": "Successfully unsubscribed from {0}.", "Info_UnsubscribeSuccessMessage": "Successfully unsubscribed from {0}.",
"Info_UnsubscribeErrorMessage": "Failed to unsubscribe", "Info_UnsubscribeErrorMessage": "Failed to unsubscribe",
"Info_CantDeletePrimaryAliasMessage": "Primary alias can't be deleted. Please change your alias before deleting this one",
"Info_MailListSizeResetSuccessMessage": "The Mail List size has been reset.",
"ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method", "ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method",
"ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security", "ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security",
"ImapAuthenticationMethod_Auto": "Auto", "ImapAuthenticationMethod_Auto": "Auto",
@@ -396,6 +423,8 @@
"SettingsFolderSync_Title": "Folder Synchronization", "SettingsFolderSync_Title": "Folder Synchronization",
"SettingsFolderOptions_Title": "Folder Configuration", "SettingsFolderOptions_Title": "Folder Configuration",
"SettingsFolderOptions_Description": "Change individual folder settings like enable/disable sync or show/hide unread badge.", "SettingsFolderOptions_Description": "Change individual folder settings like enable/disable sync or show/hide unread badge.",
"SettingsManageAliases_Title": "Aliases",
"SettingsManageAliases_Description": "See e-mail aliases assigned for this account, update or delete them.",
"SettingsHoverActionCenter": "Center Action", "SettingsHoverActionCenter": "Center Action",
"SettingsHoverActionLeft": "Left Action", "SettingsHoverActionLeft": "Left Action",
"SettingsHoverActionRight": "Right Action", "SettingsHoverActionRight": "Right Action",
@@ -406,6 +435,11 @@
"SettingsLanguageTime_Title": "Language & Time", "SettingsLanguageTime_Title": "Language & Time",
"SettingsLanguageTime_Description": "Wino display language, preferred time format.", "SettingsLanguageTime_Description": "Wino display language, preferred time format.",
"CategoriesFolderNameOverride": "Categories", "CategoriesFolderNameOverride": "Categories",
"AccountAlias_Column_Verified": "Verified",
"AccountAlias_Column_Alias": "Alias",
"AccountAlias_Column_IsPrimaryAlias": "Primary",
"AccountAlias_Disclaimer_FirstLine": "Wino can only import aliases for your Gmail accounts.",
"AccountAlias_Disclaimer_SecondLine": "If you want to use aliases for your Outlook or IMAP account, please add them yourself.",
"MoreFolderNameOverride": "More", "MoreFolderNameOverride": "More",
"SettingsOptions_Title": "Settings", "SettingsOptions_Title": "Settings",
"SettingsLinkAccounts_Description": "Merge multiple accounts into one. See mails from one Inbox together.", "SettingsLinkAccounts_Description": "Merge multiple accounts into one. See mails from one Inbox together.",
@@ -448,8 +482,8 @@
"SettingsNoAccountSetupMessage": "You didn't setup any accounts yet.", "SettingsNoAccountSetupMessage": "You didn't setup any accounts yet.",
"SettingsNotifications_Description": "Turn on or off notifications for this account.", "SettingsNotifications_Description": "Turn on or off notifications for this account.",
"SettingsNotifications_Title": "Notifications", "SettingsNotifications_Title": "Notifications",
"SettingsPaneLength_Description": "Change the width of the mail list.", "SettingsPaneLengthReset_Description": "Reset the size of the mail list to original if you have issues with it.",
"SettingsPaneLength_Title": "Mail List Pane Length", "SettingsPaneLengthReset_Title": "Reset Mail List Size",
"SettingsPaypal_Description": "Show much more love ❤️ All donations are appreciated.", "SettingsPaypal_Description": "Show much more love ❤️ All donations are appreciated.",
"SettingsPaypal_Title": "Donate via PayPal", "SettingsPaypal_Title": "Donate via PayPal",
"SettingsPersonalizationMailDisplayCompactMode": "Compact Mode", "SettingsPersonalizationMailDisplayCompactMode": "Compact Mode",

View File

@@ -38,6 +38,11 @@ namespace Wino.Core.Domain
/// </summary> /// </summary>
public static string AccountCreationDialog_SigninIn => Resources.GetTranslatedString(@"AccountCreationDialog_SigninIn"); public static string AccountCreationDialog_SigninIn => Resources.GetTranslatedString(@"AccountCreationDialog_SigninIn");
/// <summary>
/// Fetching profile details.
/// </summary>
public static string AccountCreationDialog_FetchingProfileInformation => Resources.GetTranslatedString(@"AccountCreationDialog_FetchingProfileInformation");
/// <summary> /// <summary>
/// Account Name /// Account Name
/// </summary> /// </summary>
@@ -128,6 +133,16 @@ namespace Wino.Core.Domain
/// </summary> /// </summary>
public static string Buttons_AddAccount => Resources.GetTranslatedString(@"Buttons_AddAccount"); public static string Buttons_AddAccount => Resources.GetTranslatedString(@"Buttons_AddAccount");
/// <summary>
/// Add New Alias
/// </summary>
public static string Buttons_AddNewAlias => Resources.GetTranslatedString(@"Buttons_AddNewAlias");
/// <summary>
/// Synchronize Aliases
/// </summary>
public static string Buttons_SyncAliases => Resources.GetTranslatedString(@"Buttons_SyncAliases");
/// <summary> /// <summary>
/// Apply Theme /// Apply Theme
/// </summary> /// </summary>
@@ -233,6 +248,11 @@ namespace Wino.Core.Domain
/// </summary> /// </summary>
public static string Buttons_Yes => Resources.GetTranslatedString(@"Buttons_Yes"); public static string Buttons_Yes => Resources.GetTranslatedString(@"Buttons_Yes");
/// <summary>
/// Reset
/// </summary>
public static string Buttons_Reset => Resources.GetTranslatedString(@"Buttons_Reset");
/// <summary> /// <summary>
/// Center /// Center
/// </summary> /// </summary>
@@ -343,6 +363,56 @@ namespace Wino.Core.Domain
/// </summary> /// </summary>
public static string DialogMessage_AccountLimitTitle => Resources.GetTranslatedString(@"DialogMessage_AccountLimitTitle"); public static string DialogMessage_AccountLimitTitle => Resources.GetTranslatedString(@"DialogMessage_AccountLimitTitle");
/// <summary>
/// Missing Alias
/// </summary>
public static string DialogMessage_AliasNotSelectedTitle => Resources.GetTranslatedString(@"DialogMessage_AliasNotSelectedTitle");
/// <summary>
/// You must select an alias before sending a message.
/// </summary>
public static string DialogMessage_AliasNotSelectedMessage => Resources.GetTranslatedString(@"DialogMessage_AliasNotSelectedMessage");
/// <summary>
/// Existing Alias
/// </summary>
public static string DialogMessage_AliasExistsTitle => Resources.GetTranslatedString(@"DialogMessage_AliasExistsTitle");
/// <summary>
/// This alias is already in use.
/// </summary>
public static string DialogMessage_AliasExistsMessage => Resources.GetTranslatedString(@"DialogMessage_AliasExistsMessage");
/// <summary>
/// Invalid Alias
/// </summary>
public static string DialogMessage_InvalidAliasTitle => Resources.GetTranslatedString(@"DialogMessage_InvalidAliasTitle");
/// <summary>
/// This alias is not valid. Make sure all addresses of the alias are valid e-mail addresses.
/// </summary>
public static string DialogMessage_InvalidAliasMessage => Resources.GetTranslatedString(@"DialogMessage_InvalidAliasMessage");
/// <summary>
/// Can't Delete Alias
/// </summary>
public static string DialogMessage_CantDeleteRootAliasTitle => Resources.GetTranslatedString(@"DialogMessage_CantDeleteRootAliasTitle");
/// <summary>
/// Root alias can't be deleted. This is your main identity associated with your account setup.
/// </summary>
public static string DialogMessage_CantDeleteRootAliasMessage => Resources.GetTranslatedString(@"DialogMessage_CantDeleteRootAliasMessage");
/// <summary>
/// Created New Alias
/// </summary>
public static string DialogMessage_AliasCreatedTitle => Resources.GetTranslatedString(@"DialogMessage_AliasCreatedTitle");
/// <summary>
/// New alias is succesfully created.
/// </summary>
public static string DialogMessage_AliasCreatedMessage => Resources.GetTranslatedString(@"DialogMessage_AliasCreatedMessage");
/// <summary> /// <summary>
/// Do you want to permanently delete all the mails in this folder? /// Do you want to permanently delete all the mails in this folder?
/// </summary> /// </summary>
@@ -434,7 +504,7 @@ namespace Wino.Core.Domain
public static string DialogMessage_UnlinkAccountsConfirmationTitle => Resources.GetTranslatedString(@"DialogMessage_UnlinkAccountsConfirmationTitle"); public static string DialogMessage_UnlinkAccountsConfirmationTitle => Resources.GetTranslatedString(@"DialogMessage_UnlinkAccountsConfirmationTitle");
/// <summary> /// <summary>
/// Missin Subject /// Missing Subject
/// </summary> /// </summary>
public static string DialogMessage_EmptySubjectConfirmation => Resources.GetTranslatedString(@"DialogMessage_EmptySubjectConfirmation"); public static string DialogMessage_EmptySubjectConfirmation => Resources.GetTranslatedString(@"DialogMessage_EmptySubjectConfirmation");
@@ -483,6 +553,36 @@ namespace Wino.Core.Domain
/// </summary> /// </summary>
public static string Dialog_DontAskAgain => Resources.GetTranslatedString(@"Dialog_DontAskAgain"); public static string Dialog_DontAskAgain => Resources.GetTranslatedString(@"Dialog_DontAskAgain");
/// <summary>
/// Create Account Alias
/// </summary>
public static string CreateAccountAliasDialog_Title => Resources.GetTranslatedString(@"CreateAccountAliasDialog_Title");
/// <summary>
/// Make sure your outgoing server allows sending mails from this alias.
/// </summary>
public static string CreateAccountAliasDialog_Description => Resources.GetTranslatedString(@"CreateAccountAliasDialog_Description");
/// <summary>
/// Address
/// </summary>
public static string CreateAccountAliasDialog_AliasAddress => Resources.GetTranslatedString(@"CreateAccountAliasDialog_AliasAddress");
/// <summary>
/// eg. support@mydomain.com
/// </summary>
public static string CreateAccountAliasDialog_AliasAddressPlaceholder => Resources.GetTranslatedString(@"CreateAccountAliasDialog_AliasAddressPlaceholder");
/// <summary>
/// Reply-To Address
/// </summary>
public static string CreateAccountAliasDialog_ReplyToAddress => Resources.GetTranslatedString(@"CreateAccountAliasDialog_ReplyToAddress");
/// <summary>
/// admin@mydomain.com
/// </summary>
public static string CreateAccountAliasDialog_ReplyToAddressPlaceholder => Resources.GetTranslatedString(@"CreateAccountAliasDialog_ReplyToAddressPlaceholder");
/// <summary> /// <summary>
/// Wino doesn't have it's own Discord server, but special 'wino-mail' channel is hosted at 'Developer Sanctuary' server. To get the updates about Wino please join Developer Sanctuary server and follow 'wino-mail' channel under 'Community Projects' You will be directed to server URL since Discord doesn't support channel invites. /// Wino doesn't have it's own Discord server, but special 'wino-mail' channel is hosted at 'Developer Sanctuary' server. To get the updates about Wino please join Developer Sanctuary server and follow 'wino-mail' channel under 'Community Projects' You will be directed to server URL since Discord doesn't support channel invites.
/// </summary> /// </summary>
@@ -498,6 +598,11 @@ namespace Wino.Core.Domain
/// </summary> /// </summary>
public static string Draft => Resources.GetTranslatedString(@"Draft"); public static string Draft => Resources.GetTranslatedString(@"Draft");
/// <summary>
/// Busy
/// </summary>
public static string Busy => Resources.GetTranslatedString(@"Busy");
/// <summary> /// <summary>
/// Draw /// Draw
/// </summary> /// </summary>
@@ -553,6 +658,11 @@ namespace Wino.Core.Domain
/// </summary> /// </summary>
public static string Exception_WinoServerException => Resources.GetTranslatedString(@"Exception_WinoServerException"); public static string Exception_WinoServerException => Resources.GetTranslatedString(@"Exception_WinoServerException");
/// <summary>
/// This mail is still being processed. Please try again after few seconds.
/// </summary>
public static string Exception_MailProcessing => Resources.GetTranslatedString(@"Exception_MailProcessing");
/// <summary> /// <summary>
/// Couldn't find mailbox settings. /// Couldn't find mailbox settings.
/// </summary> /// </summary>
@@ -588,6 +698,21 @@ namespace Wino.Core.Domain
/// </summary> /// </summary>
public static string Exception_FailedToSynchronizeFolders => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeFolders"); public static string Exception_FailedToSynchronizeFolders => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeFolders");
/// <summary>
/// Failed to synchronize aliases
/// </summary>
public static string Exception_FailedToSynchronizeAliases => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeAliases");
/// <summary>
/// Primary alias does not exist for this account. Creating draft failed.
/// </summary>
public static string Exception_MissingAlias => Resources.GetTranslatedString(@"Exception_MissingAlias");
/// <summary>
/// Failed to synchronize profile information
/// </summary>
public static string Exception_FailedToSynchronizeProfileInformation => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeProfileInformation");
/// <summary> /// <summary>
/// Callback uri is null on activation. /// Callback uri is null on activation.
/// </summary> /// </summary>
@@ -1288,6 +1413,16 @@ namespace Wino.Core.Domain
/// </summary> /// </summary>
public static string Info_UnsubscribeErrorMessage => Resources.GetTranslatedString(@"Info_UnsubscribeErrorMessage"); public static string Info_UnsubscribeErrorMessage => Resources.GetTranslatedString(@"Info_UnsubscribeErrorMessage");
/// <summary>
/// Primary alias can't be deleted. Please change your alias before deleting this one
/// </summary>
public static string Info_CantDeletePrimaryAliasMessage => Resources.GetTranslatedString(@"Info_CantDeletePrimaryAliasMessage");
/// <summary>
/// The Mail List size has been reset.
/// </summary>
public static string Info_MailListSizeResetSuccessMessage => Resources.GetTranslatedString(@"Info_MailListSizeResetSuccessMessage");
/// <summary> /// <summary>
/// Authentication method /// Authentication method
/// </summary> /// </summary>
@@ -2003,6 +2138,16 @@ namespace Wino.Core.Domain
/// </summary> /// </summary>
public static string SettingsFolderOptions_Description => Resources.GetTranslatedString(@"SettingsFolderOptions_Description"); public static string SettingsFolderOptions_Description => Resources.GetTranslatedString(@"SettingsFolderOptions_Description");
/// <summary>
/// Aliases
/// </summary>
public static string SettingsManageAliases_Title => Resources.GetTranslatedString(@"SettingsManageAliases_Title");
/// <summary>
/// See e-mail aliases assigned for this account, update or delete them.
/// </summary>
public static string SettingsManageAliases_Description => Resources.GetTranslatedString(@"SettingsManageAliases_Description");
/// <summary> /// <summary>
/// Center Action /// Center Action
/// </summary> /// </summary>
@@ -2053,6 +2198,31 @@ namespace Wino.Core.Domain
/// </summary> /// </summary>
public static string CategoriesFolderNameOverride => Resources.GetTranslatedString(@"CategoriesFolderNameOverride"); public static string CategoriesFolderNameOverride => Resources.GetTranslatedString(@"CategoriesFolderNameOverride");
/// <summary>
/// Verified
/// </summary>
public static string AccountAlias_Column_Verified => Resources.GetTranslatedString(@"AccountAlias_Column_Verified");
/// <summary>
/// Alias
/// </summary>
public static string AccountAlias_Column_Alias => Resources.GetTranslatedString(@"AccountAlias_Column_Alias");
/// <summary>
/// Primary
/// </summary>
public static string AccountAlias_Column_IsPrimaryAlias => Resources.GetTranslatedString(@"AccountAlias_Column_IsPrimaryAlias");
/// <summary>
/// Wino can only import aliases for your Gmail accounts.
/// </summary>
public static string AccountAlias_Disclaimer_FirstLine => Resources.GetTranslatedString(@"AccountAlias_Disclaimer_FirstLine");
/// <summary>
/// If you want to use aliases for your Outlook or IMAP account, please add them yourself.
/// </summary>
public static string AccountAlias_Disclaimer_SecondLine => Resources.GetTranslatedString(@"AccountAlias_Disclaimer_SecondLine");
/// <summary> /// <summary>
/// More /// More
/// </summary> /// </summary>
@@ -2264,14 +2434,14 @@ namespace Wino.Core.Domain
public static string SettingsNotifications_Title => Resources.GetTranslatedString(@"SettingsNotifications_Title"); public static string SettingsNotifications_Title => Resources.GetTranslatedString(@"SettingsNotifications_Title");
/// <summary> /// <summary>
/// Change the width of the mail list. /// Reset the size of the mail list to original if you have issues with it.
/// </summary> /// </summary>
public static string SettingsPaneLength_Description => Resources.GetTranslatedString(@"SettingsPaneLength_Description"); public static string SettingsPaneLengthReset_Description => Resources.GetTranslatedString(@"SettingsPaneLengthReset_Description");
/// <summary> /// <summary>
/// Mail List Pane Length /// Reset Mail List Size
/// </summary> /// </summary>
public static string SettingsPaneLength_Title => Resources.GetTranslatedString(@"SettingsPaneLength_Title"); public static string SettingsPaneLengthReset_Title => Resources.GetTranslatedString(@"SettingsPaneLengthReset_Title");
/// <summary> /// <summary>
/// Show much more love ❤️ All donations are appreciated. /// Show much more love ❤️ All donations are appreciated.

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,28 +15,9 @@ 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()
{
if (!_configurationService.Get(IsBackgroundTasksUnregisteredKey, false))
{ {
foreach (var task in BackgroundTaskRegistration.AllTasks) foreach (var task in BackgroundTaskRegistration.AllTasks)
{ {
@@ -51,19 +25,8 @@ namespace Wino.Core.UWP.Services
} }
Log.Information("Unregistered all background tasks."); Log.Information("Unregistered all background tasks.");
_configurationService.Set(IsBackgroundTasksUnregisteredKey, true);
} }
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
return false; {
Log.Information("FullTrustAppContract is not present in the system. Server connection is not possible.");
} }
public async Task<bool> DisconnectAsync() return false;
{
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

@@ -28,7 +28,7 @@ namespace Wino.Core.Authenticators
public string ClientId { get; } = "b19c2035-d740-49ff-b297-de6ec561b208"; public string ClientId { get; } = "b19c2035-d740-49ff-b297-de6ec561b208";
private readonly string[] MailScope = ["email", "mail.readwrite", "offline_access", "mail.send"]; private readonly string[] MailScope = ["email", "mail.readwrite", "offline_access", "mail.send", "Mail.Send.Shared", "Mail.ReadWrite.Shared"];
public override MailProviderType ProviderType => MailProviderType.Outlook; public override MailProviderType ProviderType => MailProviderType.Outlook;

View File

@@ -1,31 +0,0 @@
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.MenuItems;
namespace Wino.Core.Extensions
{
public static class FolderTreeExtensions
{
private static MenuItemBase<IMailItemFolder, FolderMenuItem> GetMenuItemByFolderRecursive(IMailItemFolder structure, AccountMenuItem parentAccountMenuItem, IMenuItem parentFolderItem)
{
MenuItemBase<IMailItemFolder, FolderMenuItem> parentMenuItem = new FolderMenuItem(structure, parentAccountMenuItem.Parameter, parentFolderItem);
var childStructures = structure.ChildFolders;
foreach (var childFolder in childStructures)
{
if (childFolder == null) continue;
// Folder menu item.
var subChildrenFolderTree = GetMenuItemByFolderRecursive(childFolder, parentAccountMenuItem, parentMenuItem);
if (subChildrenFolderTree is FolderMenuItem folderItem)
{
parentMenuItem.SubMenuItems.Add(folderItem);
}
}
return parentMenuItem;
}
}
}

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Web; using System.Web;
using Google.Apis.Gmail.v1.Data; using Google.Apis.Gmail.v1.Data;
@@ -205,44 +204,16 @@ namespace Wino.Core.Extensions
}; };
} }
public static Tuple<MailCopy, MimeMessage, IEnumerable<string>> GetMailDetails(this Message message) public static List<RemoteAccountAlias> GetRemoteAliases(this ListSendAsResponse response)
{ {
MimeMessage mimeMessage = message.GetGmailMimeMessage(); return response?.SendAs?.Select(a => new RemoteAccountAlias()
if (mimeMessage == null)
{ {
// This should never happen. AliasAddress = a.SendAsEmail,
Debugger.Break(); IsRootAlias = a.IsDefault.GetValueOrDefault(),
IsPrimary = a.IsPrimary.GetValueOrDefault(),
return default; ReplyToAddress = a.ReplyToAddress,
} IsVerified = a.VerificationStatus == "accepted" || a.IsDefault.GetValueOrDefault(),
}).ToList();
bool isUnread = message.GetIsUnread();
bool isFocused = message.GetIsFocused();
bool isFlagged = message.GetIsFlagged();
bool isDraft = message.GetIsDraft();
var mailCopy = new MailCopy()
{
CreationDate = mimeMessage.Date.UtcDateTime,
Subject = HttpUtility.HtmlDecode(mimeMessage.Subject),
FromName = MailkitClientExtensions.GetActualSenderName(mimeMessage),
FromAddress = MailkitClientExtensions.GetActualSenderAddress(mimeMessage),
PreviewText = HttpUtility.HtmlDecode(message.Snippet),
ThreadId = message.ThreadId,
Importance = (MailImportance)mimeMessage.Importance,
Id = message.Id,
IsDraft = isDraft,
HasAttachments = mimeMessage.Attachments.Any(),
IsRead = !isUnread,
IsFlagged = isFlagged,
IsFocused = isFocused,
InReplyTo = mimeMessage.InReplyTo,
MessageId = mimeMessage.MessageId,
References = mimeMessage.References.GetReferences()
};
return new Tuple<MailCopy, MimeMessage, IEnumerable<string>>(mailCopy, mimeMessage, message.LabelIds);
} }
} }
} }

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,5 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Graph.Models; using Microsoft.Graph.Models;
using MimeKit;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
@@ -61,5 +65,160 @@ namespace Wino.Core.Extensions
return mailCopy; return mailCopy;
} }
public static Message AsOutlookMessage(this MimeMessage mime, bool includeInternetHeaders)
{
var fromAddress = GetRecipients(mime.From).ElementAt(0);
var toAddresses = GetRecipients(mime.To).ToList();
var ccAddresses = GetRecipients(mime.Cc).ToList();
var bccAddresses = GetRecipients(mime.Bcc).ToList();
var replyToAddresses = GetRecipients(mime.ReplyTo).ToList();
var message = new Message()
{
Subject = mime.Subject,
Importance = GetImportance(mime.Importance),
Body = new ItemBody() { ContentType = BodyType.Html, Content = mime.HtmlBody },
IsDraft = false,
IsRead = true, // Sent messages are always read.
ToRecipients = toAddresses,
CcRecipients = ccAddresses,
BccRecipients = bccAddresses,
From = fromAddress,
InternetMessageId = GetProperId(mime.MessageId),
ReplyTo = replyToAddresses,
Attachments = []
};
// Headers are only included when creating the draft.
// When sending, they are not included. Graph will throw an error.
if (includeInternetHeaders)
{
message.InternetMessageHeaders = GetHeaderList(mime);
}
foreach (var part in mime.BodyParts)
{
if (part.IsAttachment)
{
// File attachment.
using var memory = new MemoryStream();
((MimePart)part).Content.DecodeTo(memory);
var bytes = memory.ToArray();
var fileAttachment = new FileAttachment()
{
ContentId = part.ContentId,
Name = part.ContentDisposition?.FileName ?? part.ContentType.Name,
ContentBytes = bytes,
};
message.Attachments.Add(fileAttachment);
}
else if (part.ContentDisposition != null && part.ContentDisposition.Disposition == "inline")
{
// Inline attachment.
using var memory = new MemoryStream();
((MimePart)part).Content.DecodeTo(memory);
var bytes = memory.ToArray();
var inlineAttachment = new FileAttachment()
{
IsInline = true,
ContentId = part.ContentId,
Name = part.ContentDisposition?.FileName ?? part.ContentType.Name,
ContentBytes = bytes
};
message.Attachments.Add(inlineAttachment);
}
}
return message;
}
#region Mime to Outlook Message Helpers
private static IEnumerable<Recipient> GetRecipients(this InternetAddressList internetAddresses)
{
foreach (var address in internetAddresses)
{
if (address is MailboxAddress mailboxAddress)
yield return new Recipient() { EmailAddress = new EmailAddress() { Address = mailboxAddress.Address, Name = mailboxAddress.Name } };
else if (address is GroupAddress groupAddress)
{
// TODO: Group addresses are not directly supported.
// It'll be individually added.
foreach (var mailbox in groupAddress.Members)
if (mailbox is MailboxAddress groupMemberMailAddress)
yield return new Recipient() { EmailAddress = new EmailAddress() { Address = groupMemberMailAddress.Address, Name = groupMemberMailAddress.Name } };
}
}
}
private static Importance? GetImportance(MessageImportance importance)
{
return importance switch
{
MessageImportance.Low => Importance.Low,
MessageImportance.Normal => Importance.Normal,
MessageImportance.High => Importance.High,
_ => null
};
}
private static List<InternetMessageHeader> GetHeaderList(this MimeMessage mime)
{
// Graph API only allows max of 5 headers.
// Here we'll try to ignore some headers that are not neccessary.
// Outlook API will generate them automatically.
// Some headers also require to start with X- or x-.
string[] headersToIgnore = ["Date", "To", "MIME-Version", "From", "Subject", "Message-Id"];
string[] headersToModify = ["In-Reply-To", "Reply-To", "References", "Thread-Topic"];
var headers = new List<InternetMessageHeader>();
int includedHeaderCount = 0;
foreach (var header in mime.Headers)
{
if (!headersToIgnore.Contains(header.Field))
{
if (headersToModify.Contains(header.Field))
{
headers.Add(new InternetMessageHeader() { Name = $"X-{header.Field}", Value = header.Value });
}
else
{
headers.Add(new InternetMessageHeader() { Name = header.Field, Value = header.Value });
}
includedHeaderCount++;
}
if (includedHeaderCount >= 5) break;
}
return headers;
}
private static string GetProperId(string id)
{
// Outlook requires some identifiers to start with "X-" or "x-".
if (string.IsNullOrEmpty(id)) return string.Empty;
if (!id.StartsWith("x-") || !id.StartsWith("X-"))
return $"X-{id}";
return id;
}
#endregion
} }
} }

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

@@ -33,11 +33,13 @@ namespace Wino.Core.Integration
// Later on maybe we can make it configurable and leave it to the user with passing // Later on maybe we can make it configurable and leave it to the user with passing
// real implementation details. // real implementation details.
private readonly ImapImplementation _implementation = new ImapImplementation() private readonly ImapImplementation _implementation = new()
{ {
Version = "1.0", Version = "1.8.0",
OS = "Windows", OS = "Windows",
Vendor = "Wino" Vendor = "Wino",
SupportUrl = "https://www.winomail.app",
Name = "Wino Mail User",
}; };
private readonly int MinimumPoolSize = 5; private readonly int MinimumPoolSize = 5;

View File

@@ -19,6 +19,7 @@ namespace Wino.Core.Integration.Processors
/// </summary> /// </summary>
public interface IDefaultChangeProcessor public interface IDefaultChangeProcessor
{ {
Task UpdateAccountAsync(MailAccount account);
Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier); Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier);
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId); Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId); Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
@@ -39,12 +40,14 @@ namespace Wino.Core.Integration.Processors
/// <returns>All folders.</returns> /// <returns>All folders.</returns>
Task<List<MailItemFolder>> GetLocalFoldersAsync(Guid accountId); Task<List<MailItemFolder>> GetLocalFoldersAsync(Guid accountId);
Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(SynchronizationOptions options); Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(SynchronizationOptions options);
Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId); Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
Task UpdateFolderLastSyncDateAsync(Guid folderId); Task UpdateFolderLastSyncDateAsync(Guid folderId);
Task<List<MailItemFolder>> GetExistingFoldersAsync(Guid accountId); Task<List<MailItemFolder>> GetExistingFoldersAsync(Guid accountId);
Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases);
} }
public interface IGmailChangeProcessor : IDefaultChangeProcessor public interface IGmailChangeProcessor : IDefaultChangeProcessor
@@ -172,5 +175,11 @@ namespace Wino.Core.Integration.Processors
public Task UpdateFolderLastSyncDateAsync(Guid folderId) public Task UpdateFolderLastSyncDateAsync(Guid folderId)
=> FolderService.UpdateFolderLastSyncDateAsync(folderId); => FolderService.UpdateFolderLastSyncDateAsync(folderId);
public Task UpdateAccountAsync(MailAccount account)
=> AccountService.UpdateAccountAsync(account);
public Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases)
=> AccountService.UpdateRemoteAliasInformationAsync(account, remoteAccountAliases);
} }
} }

View File

@@ -47,6 +47,12 @@ namespace Wino.Core.MenuItems
set => SetProperty(Parameter.Name, value, Parameter, (u, n) => u.Name = n); set => SetProperty(Parameter.Name, value, Parameter, (u, n) => u.Name = n);
} }
public string Base64ProfilePicture
{
get => Parameter.Name;
set => SetProperty(Parameter.Base64ProfilePictureData, value, Parameter, (u, n) => u.Base64ProfilePictureData = n);
}
public IEnumerable<MailAccount> HoldingAccounts => new List<MailAccount> { Parameter }; public IEnumerable<MailAccount> HoldingAccounts => new List<MailAccount> { Parameter };
public AccountMenuItem(MailAccount account, IMenuItem parent = null) : base(account, account.Id, parent) public AccountMenuItem(MailAccount account, IMenuItem parent = null) : base(account, account.Id, parent)
@@ -59,6 +65,7 @@ namespace Wino.Core.MenuItems
Parameter = account; Parameter = account;
AccountName = account.Name; AccountName = account.Name;
AttentionReason = account.AttentionReason; AttentionReason = account.AttentionReason;
Base64ProfilePicture = account.Base64ProfilePictureData;
if (SubMenuItems == null) return; if (SubMenuItems == null) return;

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,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Diagnostics; using CommunityToolkit.Diagnostics;
@@ -10,6 +9,7 @@ using SqlKata;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Extensions; using Wino.Core.Extensions;
using Wino.Messaging.Client.Accounts; using Wino.Messaging.Client.Accounts;
using Wino.Messaging.UI; using Wino.Messaging.UI;
@@ -233,6 +233,33 @@ namespace Wino.Core.Services
return accounts; return accounts;
} }
public async Task CreateRootAliasAsync(Guid accountId, string address)
{
var rootAlias = new MailAccountAlias()
{
AccountId = accountId,
AliasAddress = address,
IsPrimary = true,
IsRootAlias = true,
IsVerified = true,
ReplyToAddress = address,
Id = Guid.NewGuid()
};
await Connection.InsertAsync(rootAlias).ConfigureAwait(false);
Log.Information("Created root alias for the account {AccountId}", accountId);
}
public async Task<List<MailAccountAlias>> GetAccountAliasesAsync(Guid accountId)
{
var query = new Query(nameof(MailAccountAlias))
.Where(nameof(MailAccountAlias.AccountId), accountId)
.OrderByDesc(nameof(MailAccountAlias.IsRootAlias));
return await Connection.QueryAsync<MailAccountAlias>(query.GetRawQuery()).ConfigureAwait(false);
}
private Task<MergedInbox> GetMergedInboxInformationAsync(Guid mergedInboxId) private Task<MergedInbox> GetMergedInboxInformationAsync(Guid mergedInboxId)
=> Connection.Table<MergedInbox>().FirstOrDefaultAsync(a => a.Id == mergedInboxId); => Connection.Table<MergedInbox>().FirstOrDefaultAsync(a => a.Id == mergedInboxId);
@@ -245,6 +272,7 @@ namespace Wino.Core.Services
await Connection.Table<TokenInformation>().Where(a => a.AccountId == account.Id).DeleteAsync(); await Connection.Table<TokenInformation>().Where(a => a.AccountId == account.Id).DeleteAsync();
await Connection.Table<MailItemFolder>().DeleteAsync(a => a.MailAccountId == account.Id); await Connection.Table<MailItemFolder>().DeleteAsync(a => a.MailAccountId == account.Id);
await Connection.Table<AccountSignature>().DeleteAsync(a => a.MailAccountId == account.Id); await Connection.Table<AccountSignature>().DeleteAsync(a => a.MailAccountId == account.Id);
await Connection.Table<MailAccountAlias>().DeleteAsync(a => a.AccountId == account.Id);
// Account belongs to a merged inbox. // Account belongs to a merged inbox.
// In case of there'll be a single account in the merged inbox, remove the merged inbox as well. // In case of there'll be a single account in the merged inbox, remove the merged inbox as well.
@@ -295,6 +323,19 @@ namespace Wino.Core.Services
ReportUIChange(new AccountRemovedMessage(account)); ReportUIChange(new AccountRemovedMessage(account));
} }
public async Task UpdateProfileInformationAsync(Guid accountId, ProfileInformation profileInformation)
{
var account = await GetAccountAsync(accountId).ConfigureAwait(false);
if (account != null)
{
account.SenderName = profileInformation.SenderName;
account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
await UpdateAccountAsync(account).ConfigureAwait(false);
}
}
public async Task<MailAccount> GetAccountAsync(Guid accountId) public async Task<MailAccount> GetAccountAsync(Guid accountId)
{ {
var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId); var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);
@@ -321,17 +362,98 @@ namespace Wino.Core.Services
public async Task UpdateAccountAsync(MailAccount account) public async Task UpdateAccountAsync(MailAccount account)
{ {
if (account.Preferences == null) await Connection.UpdateAsync(account.Preferences).ConfigureAwait(false);
{ await Connection.UpdateAsync(account).ConfigureAwait(false);
Debugger.Break();
}
await Connection.UpdateAsync(account.Preferences);
await Connection.UpdateAsync(account);
ReportUIChange(new AccountUpdatedMessage(account)); ReportUIChange(new AccountUpdatedMessage(account));
} }
public async Task UpdateAccountAliasesAsync(Guid accountId, List<MailAccountAlias> aliases)
{
// Delete existing ones.
await Connection.Table<MailAccountAlias>().DeleteAsync(a => a.AccountId == accountId).ConfigureAwait(false);
// Insert new ones.
foreach (var alias in aliases)
{
await Connection.InsertAsync(alias).ConfigureAwait(false);
}
}
public async Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases)
{
var localAliases = await GetAccountAliasesAsync(account.Id).ConfigureAwait(false);
var rootAlias = localAliases.Find(a => a.IsRootAlias);
foreach (var remoteAlias in remoteAccountAliases)
{
var existingAlias = localAliases.Find(a => a.AccountId == account.Id && a.AliasAddress == remoteAlias.AliasAddress);
if (existingAlias == null)
{
// Create new alias.
var newAlias = new MailAccountAlias()
{
AccountId = account.Id,
AliasAddress = remoteAlias.AliasAddress,
IsPrimary = remoteAlias.IsPrimary,
IsVerified = remoteAlias.IsVerified,
ReplyToAddress = remoteAlias.ReplyToAddress,
Id = Guid.NewGuid(),
IsRootAlias = remoteAlias.IsRootAlias
};
await Connection.InsertAsync(newAlias);
localAliases.Add(newAlias);
}
else
{
// Update existing alias.
existingAlias.IsPrimary = remoteAlias.IsPrimary;
existingAlias.IsVerified = remoteAlias.IsVerified;
existingAlias.ReplyToAddress = remoteAlias.ReplyToAddress;
await Connection.UpdateAsync(existingAlias);
}
}
// Make sure there is only 1 root alias and 1 primary alias selected.
bool shouldUpdatePrimary = localAliases.Count(a => a.IsPrimary) != 1;
bool shouldUpdateRoot = localAliases.Count(a => a.IsRootAlias) != 1;
if (shouldUpdatePrimary)
{
localAliases.ForEach(a => a.IsPrimary = false);
var idealPrimaryAlias = localAliases.Find(a => a.AliasAddress == account.Address) ?? localAliases.First();
idealPrimaryAlias.IsPrimary = true;
await Connection.UpdateAsync(idealPrimaryAlias).ConfigureAwait(false);
}
if (shouldUpdateRoot)
{
localAliases.ForEach(a => a.IsRootAlias = false);
var idealRootAlias = localAliases.Find(a => a.AliasAddress == account.Address) ?? localAliases.First();
idealRootAlias.IsRootAlias = true;
await Connection.UpdateAsync(idealRootAlias).ConfigureAwait(false);
}
}
public async Task DeleteAccountAliasAsync(Guid aliasId)
{
// Create query to delete alias.
var query = new Query("MailAccountAlias")
.Where("Id", aliasId)
.AsDelete();
await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
}
public async Task CreateAccountAsync(MailAccount account, TokenInformation tokenInformation, CustomServerInformation customServerInformation) public async Task CreateAccountAsync(MailAccount account, TokenInformation tokenInformation, CustomServerInformation customServerInformation)
{ {
Guard.IsNotNull(account); Guard.IsNotNull(account);
@@ -385,7 +507,7 @@ namespace Wino.Core.Services
// Outlook token cache is managed by MSAL. // Outlook token cache is managed by MSAL.
// Don't save it to database. // Don't save it to database.
if (tokenInformation != null && account.ProviderType != MailProviderType.Outlook) if (tokenInformation != null && (account.ProviderType != MailProviderType.Outlook || account.ProviderType == MailProviderType.Office365))
await Connection.InsertAsync(tokenInformation); await Connection.InsertAsync(tokenInformation);
} }
@@ -437,5 +559,14 @@ namespace Wino.Core.Services
Messenger.Send(new AccountMenuItemsReordered(accountIdOrderPair)); Messenger.Send(new AccountMenuItemsReordered(accountIdOrderPair));
} }
public async Task<MailAccountAlias> GetPrimaryAccountAliasAsync(Guid accountId)
{
var aliases = await GetAccountAliasesAsync(accountId);
if (aliases == null || aliases.Count == 0) return null;
return aliases.FirstOrDefault(a => a.IsPrimary) ?? aliases.First();
}
} }
} }

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

@@ -61,7 +61,8 @@ namespace Wino.Core.Services
typeof(CustomServerInformation), typeof(CustomServerInformation),
typeof(AccountSignature), typeof(AccountSignature),
typeof(MergedInbox), typeof(MergedInbox),
typeof(MailAccountPreferences) typeof(MailAccountPreferences),
typeof(MailAccountAlias)
); );
} }
} }

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;
@@ -11,6 +10,7 @@ using SqlKata;
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;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Comparers; using Wino.Core.Domain.Models.Comparers;
@@ -32,7 +32,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 +52,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);
@@ -72,13 +63,15 @@ namespace Wino.Core.Services
// This header will be used to map the local draft copy with the remote draft copy. // This header will be used to map the local draft copy with the remote draft copy.
var mimeUniqueId = createdDraftMimeMessage.Headers[Constants.WinoLocalDraftHeader]; var mimeUniqueId = createdDraftMimeMessage.Headers[Constants.WinoLocalDraftHeader];
var primaryAlias = await _accountService.GetPrimaryAccountAliasAsync(accountId).ConfigureAwait(false);
var copy = new MailCopy var copy = new MailCopy
{ {
UniqueId = Guid.Parse(mimeUniqueId), UniqueId = Guid.Parse(mimeUniqueId),
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 = primaryAlias?.AliasAddress ?? 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 +86,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 +619,47 @@ 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() } },
}; };
var primaryAlias = await _accountService.GetPrimaryAccountAliasAsync(account.Id) ?? throw new MissingAliasException();
// Set FromName and FromAddress by alias.
message.From.Add(new MailboxAddress(account.SenderName, primaryAlias.AliasAddress));
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);
message.Body = builder.ToMessageBody();
return message;
} }
var reason = draftCreationOptions.Reason; private string CreateHtmlGap()
var referenceMessage = draftCreationOptions.ReferenceMimeMessage;
message.From.Add(new MailboxAddress(account.SenderName, account.Address));
// It contains empty blocks with inlined font, to make sure when users starts typing,it will follow selected font.
var gapHtml = CreateHtmlGap();
// Manage "To"
if (reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll)
{ {
// Reply to the sender of the message 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));
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" private async Task<string> GetSignature(MailAccount account, DraftCreationReason reason)
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 +670,96 @@ 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;
}
}
return null;
}
private MimeMessage CreateEmptyDraft(BodyBuilder builder, MimeMessage message, DraftCreationOptions draftCreationOptions, string signature)
{ {
builder.HtmlBody = $"{gapHtml}{signature.HtmlBody}"; builder.HtmlBody = CreateHtmlGap();
} if (draftCreationOptions.MailToUri != null)
else
{ {
builder.HtmlBody = $"{gapHtml}{signature.HtmlBody}{gapHtml}{builder.HtmlBody}"; if (draftCreationOptions.MailToUri.Subject != null)
} message.Subject = draftCreationOptions.MailToUri.Subject;
}
} if (draftCreationOptions.MailToUri.Body != null)
else
{ {
builder.HtmlBody = $"{gapHtml}{builder.HtmlBody}"; 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)));
}
// Self email can be present at this step, when replying to own message. It should be removed only in case there no other recipients.
if (message.To.Count > 1)
{
var self = message.To.FirstOrDefault(x => x is MailboxAddress mailboxAddress && mailboxAddress.Address.Equals(account.Address, StringComparison.OrdinalIgnoreCase));
if (self != null)
message.To.Remove(self);
}
// 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 +773,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)
@@ -836,12 +802,6 @@ namespace Wino.Core.Services
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

@@ -109,12 +109,9 @@ namespace Wino.Core.Services
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId).ConfigureAwait(false); var resourcePath = await GetMimeResourcePathAsync(accountId, fileId).ConfigureAwait(false);
var completeFilePath = GetEMLPath(resourcePath); var completeFilePath = GetEMLPath(resourcePath);
var fileStream = File.Create(completeFilePath); using var fileStream = File.Open(completeFilePath, FileMode.OpenOrCreate);
using (fileStream)
{
await mimeMessage.WriteToAsync(fileStream).ConfigureAwait(false); await mimeMessage.WriteToAsync(fileStream).ConfigureAwait(false);
}
return true; return true;
} }

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

@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
@@ -12,6 +13,7 @@ using Wino.Core.Domain;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Integration; using Wino.Core.Integration;
@@ -69,8 +71,65 @@ namespace Wino.Core.Synchronizers
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
public abstract Task ExecuteNativeRequestsAsync(IEnumerable<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default); public abstract Task ExecuteNativeRequestsAsync(IEnumerable<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
public abstract Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default); /// <summary>
/// Refreshes remote mail account profile if possible.
/// Profile picture, sender name and mailbox settings (todo) will be handled in this step.
/// </summary>
public virtual Task<ProfileInformation> GetProfileInformationAsync() => default;
/// <summary>
/// Refreshes the aliases of the account.
/// Only available for Gmail right now.
/// </summary>
protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask;
/// <summary>
/// Returns the base64 encoded profile picture of the account from the given URL.
/// </summary>
/// <param name="url">URL to retrieve picture from.</param>
/// <returns>base64 encoded profile picture</returns>
protected async Task<string> GetProfilePictureBase64EncodedAsync(string url)
{
using var client = new HttpClient();
var response = await client.GetAsync(url).ConfigureAwait(false);
var byteContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
return Convert.ToBase64String(byteContent);
}
/// <summary>
/// Internally synchronizes the account with the given options.
/// Not exposed and overriden for each synchronizer.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
protected abstract Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Safely updates account's profile information.
/// Database changes are reflected after this call.
/// </summary>
private async Task<ProfileInformation> SynchronizeProfileInformationInternalAsync()
{
var profileInformation = await GetProfileInformationAsync();
if (profileInformation != null)
{
Account.SenderName = profileInformation.SenderName;
Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
}
return profileInformation;
}
/// <summary>
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
public async Task<SynchronizationResult> SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) public async Task<SynchronizationResult> SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{ {
try try
@@ -104,6 +163,48 @@ namespace Wino.Core.Synchronizers
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken); await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
// Handle special synchronization types.
// Profile information sync.
if (options.Type == SynchronizationType.UpdateProfile)
{
if (!Account.IsProfileInfoSyncSupported) return SynchronizationResult.Empty;
ProfileInformation newProfileInformation = null;
try
{
newProfileInformation = await SynchronizeProfileInformationInternalAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failed to update profile information for {Name}", Account.Name);
return SynchronizationResult.Failed;
}
return SynchronizationResult.Completed(null, newProfileInformation);
}
// Alias sync.
if (options.Type == SynchronizationType.Alias)
{
if (!Account.IsAliasSyncSupported) return SynchronizationResult.Empty;
try
{
await SynchronizeAliasesAsync();
return SynchronizationResult.Empty;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to update aliases for {Name}", Account.Name);
return SynchronizationResult.Failed;
}
}
// Let servers to finish their job. Sometimes the servers doesn't respond immediately. // Let servers to finish their job. Sometimes the servers doesn't respond immediately.
bool shouldDelayExecution = batches.Any(a => a.DelayExecution); bool shouldDelayExecution = batches.Any(a => a.DelayExecution);
@@ -150,6 +251,10 @@ namespace Wino.Core.Synchronizers
private void PublishUnreadItemChanges() private void PublishUnreadItemChanges()
=> WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(Account.Id)); => WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(Account.Id));
/// <summary>
/// Sends a message to the shell to update the synchronization progress.
/// </summary>
/// <param name="progress">Percentage of the progress.</param>
public void PublishSynchronizationProgress(double progress) public void PublishSynchronizationProgress(double progress)
=> WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress)); => WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress));

View File

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Google.Apis.Gmail.v1; using Google.Apis.Gmail.v1;
using Google.Apis.Gmail.v1.Data; using Google.Apis.Gmail.v1.Data;
using Google.Apis.Http; using Google.Apis.Http;
using Google.Apis.PeopleService.v1;
using Google.Apis.Requests; using Google.Apis.Requests;
using Google.Apis.Services; using Google.Apis.Services;
using MailKit; using MailKit;
@@ -18,6 +19,7 @@ using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Requests; using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Domain.Models.Synchronization;
@@ -37,8 +39,10 @@ namespace Wino.Core.Synchronizers
// https://github.com/googleapis/google-api-dotnet-client/issues/2603 // https://github.com/googleapis/google-api-dotnet-client/issues/2603
private const uint MaximumAllowedBatchRequestSize = 10; private const uint MaximumAllowedBatchRequestSize = 10;
private readonly ConfigurableHttpClient _gmailHttpClient; private readonly ConfigurableHttpClient _googleHttpClient;
private readonly GmailService _gmailService; private readonly GmailService _gmailService;
private readonly PeopleServiceService _peopleService;
private readonly IAuthenticator _authenticator; private readonly IAuthenticator _authenticator;
private readonly IGmailChangeProcessor _gmailChangeProcessor; private readonly IGmailChangeProcessor _gmailChangeProcessor;
private readonly ILogger _logger = Log.ForContext<GmailSynchronizer>(); private readonly ILogger _logger = Log.ForContext<GmailSynchronizer>();
@@ -54,15 +58,48 @@ namespace Wino.Core.Synchronizers
HttpClientFactory = this HttpClientFactory = this
}; };
_gmailHttpClient = new ConfigurableHttpClient(messageHandler); _googleHttpClient = new ConfigurableHttpClient(messageHandler);
_gmailService = new GmailService(initializer); _gmailService = new GmailService(initializer);
_peopleService = new PeopleServiceService(initializer);
_authenticator = authenticator; _authenticator = authenticator;
_gmailChangeProcessor = gmailChangeProcessor; _gmailChangeProcessor = gmailChangeProcessor;
} }
public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) => _gmailHttpClient; public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) => _googleHttpClient;
public override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) public override async Task<ProfileInformation> GetProfileInformationAsync()
{
var profileRequest = _peopleService.People.Get("people/me");
profileRequest.PersonFields = "names,photos";
string senderName = string.Empty, base64ProfilePicture = string.Empty;
var userProfile = await profileRequest.ExecuteAsync();
senderName = userProfile.Names?.FirstOrDefault()?.DisplayName ?? Account.SenderName;
var profilePicture = userProfile.Photos?.FirstOrDefault()?.Url ?? string.Empty;
if (!string.IsNullOrEmpty(profilePicture))
{
base64ProfilePicture = await GetProfilePictureBase64EncodedAsync(profilePicture).ConfigureAwait(false);
}
return new ProfileInformation(senderName, base64ProfilePicture);
}
protected override async Task SynchronizeAliasesAsync()
{
var sendAsListRequest = _gmailService.Users.Settings.SendAs.List("me");
var sendAsListResponse = await sendAsListRequest.ExecuteAsync();
var remoteAliases = sendAsListResponse.GetRemoteAliases();
await _gmailChangeProcessor.UpdateRemoteAliasInformationAsync(Account, remoteAliases).ConfigureAwait(false);
}
protected override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{ {
_logger.Information("Internal synchronization started for {Name}", Account.Name); _logger.Information("Internal synchronization started for {Name}", Account.Name);

View File

@@ -405,7 +405,7 @@ namespace Wino.Core.Synchronizers
]; ];
} }
public override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) protected override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{ {
var downloadedMessageIds = new List<string>(); var downloadedMessageIds = new List<string>();
@@ -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

@@ -4,8 +4,8 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Text; using System.Text;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -18,11 +18,11 @@ using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
using MimeKit; using MimeKit;
using MoreLinq.Extensions; using MoreLinq.Extensions;
using Serilog; using Serilog;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Requests; using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Domain.Models.Synchronization;
@@ -128,7 +128,7 @@ namespace Wino.Core.Synchronizers
#endregion #endregion
public override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) protected override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{ {
var downloadedMessageIds = new List<string>(); var downloadedMessageIds = new List<string>();
@@ -473,6 +473,40 @@ namespace Wino.Core.Synchronizers
} }
} }
/// <summary>
/// Get the user's profile picture
/// </summary>
/// <returns>Base64 encoded profile picture.</returns>
private async Task<string> GetUserProfilePictureAsync()
{
var photoStream = await _graphClient.Me.Photos["48x48"].Content.GetAsync();
using var memoryStream = new MemoryStream();
await photoStream.CopyToAsync(memoryStream);
var byteArray = memoryStream.ToArray();
return Convert.ToBase64String(byteArray);
}
/// <summary>
/// Get the user's display name.
/// </summary>
/// <returns>Display name of the user.</returns>
private async Task<string> GetSenderNameAsync()
{
var userInfo = await _graphClient.Users["me"].GetAsync();
return userInfo.DisplayName;
}
public override async Task<ProfileInformation> GetProfileInformationAsync()
{
var profilePictureData = await GetUserProfilePictureAsync().ConfigureAwait(false);
var senderName = await GetSenderNameAsync().ConfigureAwait(false);
return new ProfileInformation(senderName, profilePictureData);
}
#region Mail Integration #region Mail Integration
public override bool DelaySendOperationSynchronization() => true; public override bool DelaySendOperationSynchronization() => true;
@@ -572,22 +606,37 @@ namespace Wino.Core.Synchronizers
{ {
if (item is CreateDraftRequest createDraftRequest) if (item is CreateDraftRequest createDraftRequest)
{ {
createDraftRequest.DraftPreperationRequest.CreatedLocalDraftMimeMessage.Prepare(EncodingConstraint.None); var reason = createDraftRequest.DraftPreperationRequest.Reason;
var message = createDraftRequest.DraftPreperationRequest.CreatedLocalDraftMimeMessage.AsOutlookMessage(true);
var plainTextBytes = Encoding.UTF8.GetBytes(createDraftRequest.DraftPreperationRequest.CreatedLocalDraftMimeMessage.ToString()); if (reason == DraftCreationReason.Empty)
var base64Encoded = Convert.ToBase64String(plainTextBytes); {
return _graphClient.Me.Messages.ToPostRequestInformation(message);
var requestInformation = _graphClient.Me.Messages.ToPostRequestInformation(new Message()); }
else if (reason == DraftCreationReason.Reply)
requestInformation.Headers.Clear();// replace the json content header {
requestInformation.Headers.Add("Content-Type", "text/plain"); return _graphClient.Me.Messages[createDraftRequest.DraftPreperationRequest.ReferenceMailCopy.Id].CreateReply.ToPostRequestInformation(new Microsoft.Graph.Me.Messages.Item.CreateReply.CreateReplyPostRequestBody()
{
requestInformation.SetStreamContent(new MemoryStream(Encoding.UTF8.GetBytes(base64Encoded)), "text/plain"); Message = message
});
return requestInformation; }
else if (reason == DraftCreationReason.ReplyAll)
{
return _graphClient.Me.Messages[createDraftRequest.DraftPreperationRequest.ReferenceMailCopy.Id].CreateReplyAll.ToPostRequestInformation(new Microsoft.Graph.Me.Messages.Item.CreateReplyAll.CreateReplyAllPostRequestBody()
{
Message = message
});
}
else if (reason == DraftCreationReason.Forward)
{
return _graphClient.Me.Messages[createDraftRequest.DraftPreperationRequest.ReferenceMailCopy.Id].CreateForward.ToPostRequestInformation(new Microsoft.Graph.Me.Messages.Item.CreateForward.CreateForwardPostRequestBody()
{
Message = message
});
}
} }
return default; throw new Exception("Invalid create draft request type.");
}); });
} }
@@ -602,50 +651,43 @@ namespace Wino.Core.Synchronizers
var mailCopyId = sendDraftPreparationRequest.MailItem.Id; var mailCopyId = sendDraftPreparationRequest.MailItem.Id;
var mimeMessage = sendDraftPreparationRequest.Mime; var mimeMessage = sendDraftPreparationRequest.Mime;
var batchDeleteRequest = new BatchDeleteRequest(new List<IRequest>() // Convert mime message to Outlook message.
// Outlook synchronizer does not send MIME messages directly anymore.
// Alias support is lacking with direct MIMEs.
// Therefore we convert the MIME message to Outlook message and use proper APIs.
var outlookMessage = mimeMessage.AsOutlookMessage(false);
// Update draft.
var patchDraftRequest = _graphClient.Me.Messages[mailCopyId].ToPatchRequestInformation(outlookMessage);
var patchDraftRequestBundle = new HttpRequestBundle<RequestInformation>(patchDraftRequest, request);
// Send draft.
// POST requests are handled differently in batches in Graph SDK.
// Batch basically ignores the step's coontent-type and body.
// Manually create a POST request with empty body and send it.
var sendDraftRequest = _graphClient.Me.Messages[mailCopyId].Send.ToPostRequestInformation((config) =>
{ {
new DeleteRequest(sendDraftPreparationRequest.MailItem) config.Headers.Add("Content-Type", "application/json");
}); });
var deleteBundle = Delete(batchDeleteRequest).ElementAt(0); sendDraftRequest.Headers.Clear();
mimeMessage.Prepare(EncodingConstraint.None); sendDraftRequest.Content = new MemoryStream(Encoding.UTF8.GetBytes("{}"));
sendDraftRequest.HttpMethod = Method.POST;
sendDraftRequest.Headers.Add("Content-Type", "application/json");
var plainTextBytes = Encoding.UTF8.GetBytes(mimeMessage.ToString()); var sendDraftRequestBundle = new HttpRequestBundle<RequestInformation>(sendDraftRequest, request);
var base64Encoded = Convert.ToBase64String(plainTextBytes);
var outlookMessage = new Message() return [patchDraftRequestBundle, sendDraftRequestBundle];
{
ConversationId = sendDraftPreparationRequest.MailItem.ThreadId
};
// Apply importance here as well just in case.
if (mimeMessage.Importance != MessageImportance.Normal)
outlookMessage.Importance = mimeMessage.Importance == MessageImportance.High ? Importance.High : Importance.Low;
var body = new Microsoft.Graph.Me.SendMail.SendMailPostRequestBody()
{
Message = outlookMessage
};
var sendRequest = _graphClient.Me.SendMail.ToPostRequestInformation(body);
sendRequest.Headers.Clear();
sendRequest.Headers.Add("Content-Type", "text/plain");
var stream = new MemoryStream(Encoding.UTF8.GetBytes(base64Encoded));
sendRequest.SetStreamContent(stream, "text/plain");
var sendMailRequest = new HttpRequestBundle<RequestInformation>(sendRequest, request);
return [deleteBundle, sendMailRequest];
} }
public override IEnumerable<IRequestBundle<RequestInformation>> Archive(BatchArchiveRequest request) public override IEnumerable<IRequestBundle<RequestInformation>> Archive(BatchArchiveRequest request)
=> Move(new BatchMoveRequest(request.Items, request.FromFolder, request.ToFolder)); => Move(new BatchMoveRequest(request.Items, request.FromFolder, request.ToFolder));
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem, public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem,
MailKit.ITransferProgress transferProgress = null, MailKit.ITransferProgress transferProgress = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
@@ -697,26 +739,41 @@ namespace Wino.Core.Synchronizers
request.ApplyUIChanges(); request.ApplyUIChanges();
await batchContent.AddBatchRequestStepAsync(nativeRequest).ConfigureAwait(false); var batchRequestId = await batchContent.AddBatchRequestStepAsync(nativeRequest).ConfigureAwait(false);
// Map BundleId to batch request step's key. // Map BundleId to batch request step's key.
// This is how we can identify which step succeeded or failed in the bundle. // This is how we can identify which step succeeded or failed in the bundle.
bundle.BundleId = batchContent.BatchRequestSteps.ElementAt(i).Key; bundle.BundleId = batchRequestId;
} }
if (!batchContent.BatchRequestSteps.Any()) if (!batchContent.BatchRequestSteps.Any())
continue; continue;
// Set execution type to serial instead of parallel if needed.
// Each step will depend on the previous one.
if (itemCount > 1)
{
for (int i = 1; i < itemCount; i++)
{
var currentStep = batchContent.BatchRequestSteps.ElementAt(i);
var previousStep = batchContent.BatchRequestSteps.ElementAt(i - 1);
currentStep.Value.DependsOn = [previousStep.Key];
}
}
// Execute batch. This will collect responses from network call for each batch step. // Execute batch. This will collect responses from network call for each batch step.
var batchRequestResponse = await _graphClient.Batch.PostAsync(batchContent).ConfigureAwait(false); var batchRequestResponse = await _graphClient.Batch.PostAsync(batchContent, cancellationToken).ConfigureAwait(false);
// Check responses for each bundle id. // Check responses for each bundle id.
// Each bundle id must return some HttpResponseMessage ideally. // Each bundle id must return some HttpResponseMessage ideally.
var bundleIds = batchContent.BatchRequestSteps.Select(a => a.Key); var bundleIds = batchContent.BatchRequestSteps.Select(a => a.Key);
// TODO: Handling responses. They used to work in v1 core, but not in v2. var exceptionBag = new List<string>();
foreach (var bundleId in bundleIds) foreach (var bundleId in bundleIds)
{ {
@@ -727,45 +784,31 @@ namespace Wino.Core.Synchronizers
var httpResponseMessage = await batchRequestResponse.GetResponseByIdAsync(bundleId); var httpResponseMessage = await batchRequestResponse.GetResponseByIdAsync(bundleId);
if (httpResponseMessage == null)
continue;
using (httpResponseMessage) using (httpResponseMessage)
{ {
await ProcessSingleNativeRequestResponseAsync(bundle, httpResponseMessage, cancellationToken).ConfigureAwait(false);
}
}
}
}
private async Task ProcessSingleNativeRequestResponseAsync(IRequestBundle<RequestInformation> bundle,
HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default)
{
if (httpResponseMessage == null) return;
if (!httpResponseMessage.IsSuccessStatusCode) if (!httpResponseMessage.IsSuccessStatusCode)
{ {
throw new SynchronizerException(string.Format(Translator.Exception_SynchronizerFailureHTTP, httpResponseMessage.StatusCode)); var content = await httpResponseMessage.Content.ReadAsStringAsync();
var errorJson = JsonObject.Parse(content);
var errorString = $"({httpResponseMessage.StatusCode}) {errorJson["error"]["code"]} - {errorJson["error"]["message"]}";
exceptionBag.Add(errorString);
} }
else if (bundle is HttpRequestBundle<RequestInformation, Message> messageBundle) }
}
if (exceptionBag.Any())
{ {
var outlookMessage = await messageBundle.DeserializeBundleAsync(httpResponseMessage, cancellationToken); var formattedErrorString = string.Join("\n", exceptionBag.Select((item, index) => $"{index + 1}. {item}"));
if (outlookMessage == null) return; throw new SynchronizerException(formattedErrorString);
// TODO: Handle new message added or updated.
}
else if (bundle is HttpRequestBundle<RequestInformation, Microsoft.Graph.Models.MailFolder> folderBundle)
{
var outlookFolder = await folderBundle.DeserializeBundleAsync(httpResponseMessage, cancellationToken);
if (outlookFolder == null) return;
// TODO: Handle new folder added or updated.
}
else if (bundle is HttpRequestBundle<RequestInformation, MimeMessage> mimeBundle)
{
// TODO: Handle mime retrieve message.
} }
} }
}
private async Task<MimeMessage> DownloadMimeMessageAsync(string messageId, CancellationToken cancellationToken = default) private async Task<MimeMessage> DownloadMimeMessageAsync(string messageId, CancellationToken cancellationToken = default)
{ {

View File

@@ -17,7 +17,8 @@
<PackageReference Include="CommunityToolkit.Diagnostics" Version="8.2.2" /> <PackageReference Include="CommunityToolkit.Diagnostics" Version="8.2.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="Google.Apis.Gmail.v1" Version="1.68.0.3427" /> <PackageReference Include="Google.Apis.Gmail.v1" Version="1.68.0.3427" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.59" /> <PackageReference Include="Google.Apis.PeopleService.v1" Version="1.68.0.3359" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.63" />
<PackageReference Include="HtmlKit" Version="1.1.0" /> <PackageReference Include="HtmlKit" Version="1.1.0" />
<PackageReference Include="IsExternalInit" Version="1.0.3"> <PackageReference Include="IsExternalInit" Version="1.0.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@@ -26,12 +27,11 @@
<PackageReference Include="MailKit" Version="4.7.1.1" /> <PackageReference Include="MailKit" Version="4.7.1.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Graph" Version="5.56.0" /> <PackageReference Include="Microsoft.Graph" Version="5.56.0" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.62.0" /> <PackageReference Include="Microsoft.Identity.Client" Version="4.63.0" />
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.62.0" /> <PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.63.0" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.62.0" /> <PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.63.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

@@ -56,7 +56,11 @@ namespace Wino.Mail.ViewModels
[RelayCommand] [RelayCommand]
private void EditSignature() private void EditSignature()
=> Messenger.Send(new BreadcrumbNavigationRequested("Signature", WinoPage.SignatureManagementPage, Account.Id)); => Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsSignature_Title, WinoPage.SignatureManagementPage, Account.Id));
[RelayCommand]
private void EditAliases()
=> Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAliases_Title, WinoPage.AliasManagementPage, Account.Id));
public Task FolderSyncToggledAsync(IMailItemFolder folderStructure, bool isEnabled) public Task FolderSyncToggledAsync(IMailItemFolder folderStructure, bool isEnabled)
=> _folderService.ChangeFolderSynchronizationStateAsync(folderStructure.Id, isEnabled); => _folderService.ChangeFolderSynchronizationStateAsync(folderStructure.Id, isEnabled);

View File

@@ -154,15 +154,12 @@ namespace Wino.Mail.ViewModels
{ {
creationDialog = _dialogService.GetAccountCreationDialog(accountCreationDialogResult.ProviderType); creationDialog = _dialogService.GetAccountCreationDialog(accountCreationDialogResult.ProviderType);
// _accountService.ExternalAuthenticationAuthenticator = _authenticationProvider.GetAuthenticator(accountCreationDialogResult.ProviderType);
CustomServerInformation customServerInformation = null; CustomServerInformation customServerInformation = null;
createdAccount = new MailAccount() createdAccount = new MailAccount()
{ {
ProviderType = accountCreationDialogResult.ProviderType, ProviderType = accountCreationDialogResult.ProviderType,
Name = accountCreationDialogResult.AccountName, Name = accountCreationDialogResult.AccountName,
SenderName = accountCreationDialogResult.SenderName,
AccountColorHex = accountCreationDialogResult.AccountColorHex, AccountColorHex = accountCreationDialogResult.AccountColorHex,
Id = Guid.NewGuid() Id = Guid.NewGuid()
}; };
@@ -208,30 +205,83 @@ namespace Wino.Mail.ViewModels
await _accountService.CreateAccountAsync(createdAccount, tokenInformation, customServerInformation); await _accountService.CreateAccountAsync(createdAccount, tokenInformation, customServerInformation);
// Local account has been created. // Local account has been created.
// Create new synchronizer and start synchronization.
// Sync profile information if supported.
if (createdAccount.IsProfileInfoSyncSupported)
{
// Start profile information synchronization.
// It's only available for Outlook and Gmail synchronizers.
var profileSyncOptions = new SynchronizationOptions()
{
AccountId = createdAccount.Id,
Type = SynchronizationType.UpdateProfile
};
var profileSynchronizationResponse = await _winoServerConnectionManager.GetResponseAsync<SynchronizationResult, NewSynchronizationRequested>(new NewSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client));
var profileSynchronizationResult = profileSynchronizationResponse.Data;
if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName;
createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData;
await _accountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation);
}
if (creationDialog is ICustomServerAccountCreationDialog customServerAccountCreationDialog) if (creationDialog is ICustomServerAccountCreationDialog customServerAccountCreationDialog)
customServerAccountCreationDialog.ShowPreparingFolders(); customServerAccountCreationDialog.ShowPreparingFolders();
else else
creationDialog.State = AccountCreationDialogState.PreparingFolders; creationDialog.State = AccountCreationDialogState.PreparingFolders;
var options = new SynchronizationOptions() // Start synchronizing folders.
var folderSyncOptions = new SynchronizationOptions()
{ {
AccountId = createdAccount.Id, AccountId = createdAccount.Id,
Type = SynchronizationType.FoldersOnly Type = SynchronizationType.FoldersOnly
}; };
var synchronizationResultResponse = await _winoServerConnectionManager.GetResponseAsync<SynchronizationResult, NewSynchronizationRequested>(new NewSynchronizationRequested(options, SynchronizationSource.Client)); var folderSynchronizationResponse = await _winoServerConnectionManager.GetResponseAsync<SynchronizationResult, NewSynchronizationRequested>(new NewSynchronizationRequested(folderSyncOptions, SynchronizationSource.Client));
var synchronizationResult = synchronizationResultResponse.Data; var folderSynchronizationResult = folderSynchronizationResponse.Data;
if (synchronizationResult.CompletedState != SynchronizationCompletedState.Success)
if (folderSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders); throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
// Check if Inbox folder is available for the account after synchronization. // Sync aliases if supported.
var isInboxAvailable = await _folderService.IsInboxAvailableForAccountAsync(createdAccount.Id); if (createdAccount.IsAliasSyncSupported)
{
// Try to synchronize aliases for the account.
if (!isInboxAvailable) var aliasSyncOptions = new SynchronizationOptions()
throw new Exception(Translator.Exception_InboxNotAvailable); {
AccountId = createdAccount.Id,
Type = SynchronizationType.Alias
};
var aliasSyncResponse = await _winoServerConnectionManager.GetResponseAsync<SynchronizationResult, NewSynchronizationRequested>(new NewSynchronizationRequested(aliasSyncOptions, SynchronizationSource.Client));
var aliasSynchronizationResult = folderSynchronizationResponse.Data;
if (aliasSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeAliases);
}
else
{
// Create root primary alias for the account.
// This is only available for accounts that do not support alias synchronization.
await _accountService.CreateRootAliasAsync(createdAccount.Id, createdAccount.Address);
}
// TODO: Temporary disabled. Is this even needed? Users can configure special folders manually later on if discovery fails.
// Check if Inbox folder is available for the account after synchronization.
//var isInboxAvailable = await _folderService.IsInboxAvailableForAccountAsync(createdAccount.Id);
//if (!isInboxAvailable)
// throw new Exception(Translator.Exception_InboxNotAvailable);
// Send changes to listeners. // Send changes to listeners.
ReportUIChange(new AccountCreatedMessage(createdAccount)); ReportUIChange(new AccountCreatedMessage(createdAccount));

View File

@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using EmailValidation;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Messaging.Server;
namespace Wino.Mail.ViewModels
{
public partial class AliasManagementPageViewModel : BaseViewModel
{
private readonly IAccountService _accountService;
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanSynchronizeAliases))]
private MailAccount account;
[ObservableProperty]
private List<MailAccountAlias> accountAliases = [];
public bool CanSynchronizeAliases => Account?.IsAliasSyncSupported ?? false;
public AliasManagementPageViewModel(IDialogService dialogService,
IAccountService accountService,
IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService)
{
_accountService = accountService;
_winoServerConnectionManager = winoServerConnectionManager;
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
if (parameters is Guid accountId)
Account = await _accountService.GetAccountAsync(accountId);
if (Account == null) return;
await LoadAliasesAsync();
}
private async Task LoadAliasesAsync()
{
AccountAliases = await _accountService.GetAccountAliasesAsync(Account.Id);
}
[RelayCommand]
private async Task SetAliasPrimaryAsync(MailAccountAlias alias)
{
if (alias.IsPrimary) return;
AccountAliases.ForEach(a =>
{
a.IsPrimary = a == alias;
});
await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases);
await LoadAliasesAsync();
}
[RelayCommand]
private async Task SyncAliasesAsync()
{
if (!CanSynchronizeAliases) return;
var aliasSyncOptions = new SynchronizationOptions()
{
AccountId = Account.Id,
Type = SynchronizationType.Alias
};
var aliasSyncResponse = await _winoServerConnectionManager.GetResponseAsync<SynchronizationResult, NewSynchronizationRequested>(new NewSynchronizationRequested(aliasSyncOptions, SynchronizationSource.Client));
if (aliasSyncResponse.IsSuccess)
await LoadAliasesAsync();
else
DialogService.InfoBarMessage(Translator.GeneralTitle_Error, aliasSyncResponse.Message, InfoBarMessageType.Error);
}
[RelayCommand]
private async Task AddNewAliasAsync()
{
var createdAliasDialog = await DialogService.ShowCreateAccountAliasDialogAsync();
if (createdAliasDialog.CreatedAccountAlias == null) return;
var newAlias = createdAliasDialog.CreatedAccountAlias;
// Check existence.
if (AccountAliases.Any(a => a.AliasAddress == newAlias.AliasAddress))
{
await DialogService.ShowMessageAsync(Translator.DialogMessage_AliasExistsTitle, Translator.DialogMessage_AliasExistsMessage);
return;
}
// Validate all addresses.
if (!EmailValidator.Validate(newAlias.AliasAddress) || (!string.IsNullOrEmpty(newAlias.ReplyToAddress) && !EmailValidator.Validate(newAlias.ReplyToAddress)))
{
await DialogService.ShowMessageAsync(Translator.DialogMessage_InvalidAliasMessage, Translator.DialogMessage_InvalidAliasTitle);
return;
}
newAlias.AccountId = Account.Id;
AccountAliases.Add(newAlias);
await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases);
DialogService.InfoBarMessage(Translator.DialogMessage_AliasCreatedTitle, Translator.DialogMessage_AliasCreatedMessage, InfoBarMessageType.Success);
await LoadAliasesAsync();
}
[RelayCommand]
private async Task DeleteAliasAsync(MailAccountAlias alias)
{
// Primary aliases can't be deleted.
if (alias.IsPrimary)
{
await DialogService.ShowMessageAsync(Translator.Info_CantDeletePrimaryAliasMessage, Translator.GeneralTitle_Warning);
return;
}
// Root aliases can't be deleted.
if (alias.IsRootAlias)
{
await DialogService.ShowMessageAsync(Translator.DialogMessage_CantDeleteRootAliasTitle, Translator.DialogMessage_CantDeleteRootAliasMessage);
return;
}
await _accountService.DeleteAccountAliasAsync(alias.Id);
await LoadAliasesAsync();
}
}
}

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)
{ {
@@ -728,18 +728,27 @@ namespace Wino.Mail.ViewModels
if (accounts.Count() == 1) if (accounts.Count() == 1)
operationAccount = accounts.FirstOrDefault(); operationAccount = accounts.FirstOrDefault();
else else
{
if (latestSelectedAccountMenuItem is MergedAccountMenuItem selectedMergedAccountMenuItem)
{ {
// There are multiple accounts and there is no selection. // There are multiple accounts and there is no selection.
// Don't list all accounts, but only accounts that belong to Merged Inbox. // Don't list all accounts, but only accounts that belong to Merged Inbox.
if (latestSelectedAccountMenuItem is MergedAccountMenuItem selectedMergedAccountMenuItem)
{
var mergedAccounts = accounts.Where(a => a.MergedInboxId == selectedMergedAccountMenuItem.EntityId); var mergedAccounts = accounts.Where(a => a.MergedInboxId == selectedMergedAccountMenuItem.EntityId);
if (!mergedAccounts.Any()) return; if (!mergedAccounts.Any()) return;
Messenger.Send(new CreateNewMailWithMultipleAccountsRequested(mergedAccounts.ToList())); Messenger.Send(new CreateNewMailWithMultipleAccountsRequested(mergedAccounts.ToList()));
} }
else if (latestSelectedAccountMenuItem is AccountMenuItem selectedAccountMenuItem)
{
operationAccount = selectedAccountMenuItem.HoldingAccounts.ElementAt(0);
}
else
{
// User is at some other page. List all accounts.
Messenger.Send(new CreateNewMailWithMultipleAccountsRequested(accounts));
}
} }
} }
@@ -774,16 +783,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, draftOptions.Reason);
await _winoRequestDelegator.ExecuteAsync(draftPreperationRequest); await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest);
} }
protected override async void OnAccountUpdated(MailAccount updatedAccount) protected override async void OnAccountUpdated(MailAccount updatedAccount)
@@ -798,7 +804,7 @@ namespace Wino.Mail.ViewModels
} }
protected override void OnAccountRemoved(MailAccount removedAccount) protected override void OnAccountRemoved(MailAccount removedAccount)
=> Messenger.Send(new AccountsMenuRefreshRequested(true)); => Messenger.Send(new AccountsMenuRefreshRequested(false));
protected override async void OnAccountCreated(MailAccount createdAccount) protected override async void OnAccountCreated(MailAccount createdAccount)
{ {
@@ -880,12 +886,9 @@ namespace Wino.Mail.ViewModels
{ {
await RecreateMenuItemsAsync(); await RecreateMenuItemsAsync();
if (message.AutomaticallyNavigateFirstItem)
{
if (MenuItems.FirstOrDefault(a => a is IAccountMenuItem) is IAccountMenuItem firstAccount) if (MenuItems.FirstOrDefault(a => a is IAccountMenuItem) is IAccountMenuItem firstAccount)
{ {
await ChangeLoadedAccountAsync(firstAccount); await ChangeLoadedAccountAsync(firstAccount, message.AutomaticallyNavigateFirstItem);
}
} }
} }

View File

@@ -3,7 +3,6 @@ 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;
@@ -21,6 +20,7 @@ using Wino.Core.Extensions;
using Wino.Core.Services; using Wino.Core.Services;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
using Wino.Messaging.Server;
namespace Wino.Mail.ViewModels namespace Wino.Mail.ViewModels
{ {
@@ -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;
@@ -68,6 +68,12 @@ namespace Wino.Mail.ViewModels
[NotifyCanExecuteChangedFor(nameof(SendCommand))] [NotifyCanExecuteChangedFor(nameof(SendCommand))]
private MailAccount composingAccount; private MailAccount composingAccount;
[ObservableProperty]
private List<MailAccountAlias> availableAliases;
[ObservableProperty]
private MailAccountAlias selectedAlias;
[ObservableProperty] [ObservableProperty]
private bool isDraggingOverComposerGrid; private bool isDraggingOverComposerGrid;
@@ -77,21 +83,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;
@@ -113,6 +118,7 @@ namespace Wino.Mail.ViewModels
private readonly IWinoRequestDelegator _worker; private readonly IWinoRequestDelegator _worker;
public readonly IFontService FontService; public readonly IFontService FontService;
public readonly IPreferencesService PreferencesService; public readonly IPreferencesService PreferencesService;
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
public readonly IContactService ContactService; public readonly IContactService ContactService;
public ComposePageViewModel(IDialogService dialogService, public ComposePageViewModel(IDialogService dialogService,
@@ -125,21 +131,23 @@ namespace Wino.Mail.ViewModels
IWinoRequestDelegator worker, IWinoRequestDelegator worker,
IContactService contactService, IContactService contactService,
IFontService fontService, IFontService fontService,
IPreferencesService preferencesService) : base(dialogService) IPreferencesService preferencesService,
IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService)
{ {
NativeAppService = nativeAppService; NativeAppService = nativeAppService;
_folderService = folderService;
ContactService = contactService; ContactService = contactService;
FontService = fontService; FontService = fontService;
PreferencesService = preferencesService;
_folderService = folderService;
_mailService = mailService; _mailService = mailService;
_launchProtocolService = launchProtocolService; _launchProtocolService = launchProtocolService;
_mimeFileService = mimeFileService; _mimeFileService = mimeFileService;
_accountService = accountService; _accountService = accountService;
_worker = worker; _worker = worker;
_winoServerConnectionManager = winoServerConnectionManager;
SelectedToolbarSection = ToolbarSections[0]; SelectedToolbarSection = ToolbarSections[0];
PreferencesService = preferencesService;
} }
[RelayCommand] [RelayCommand]
@@ -164,6 +172,12 @@ namespace Wino.Mail.ViewModels
if (!isConfirmed) return; if (!isConfirmed) return;
} }
if (SelectedAlias == null)
{
DialogService.InfoBarMessage(Translator.DialogMessage_AliasNotSelectedTitle, Translator.DialogMessage_AliasNotSelectedMessage, InfoBarMessageType.Error);
return;
}
// Save mime changes before sending. // Save mime changes before sending.
await UpdateMimeChangesAsync().ConfigureAwait(false); await UpdateMimeChangesAsync().ConfigureAwait(false);
@@ -178,11 +192,26 @@ namespace Wino.Mail.ViewModels
int count = (int)memoryStream.Length; int count = (int)memoryStream.Length;
var base64EncodedMessage = Convert.ToBase64String(buffer); var base64EncodedMessage = Convert.ToBase64String(buffer);
var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy, sentFolder, CurrentMailDraftItem.AssignedFolder, CurrentMailDraftItem.AssignedAccount.Preferences, base64EncodedMessage); var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy,
SelectedAlias,
sentFolder,
CurrentMailDraftItem.AssignedFolder,
CurrentMailDraftItem.AssignedAccount.Preferences,
base64EncodedMessage);
await _worker.ExecuteAsync(draftSendPreparationRequest); await _worker.ExecuteAsync(draftSendPreparationRequest);
} }
public async Task IncludeAttachmentAsync(MailAttachmentViewModel viewModel)
{
//if (bodyBuilder == null) return;
//bodyBuilder.Attachments.Add(viewModel.FileName, new MemoryStream(viewModel.Content));
//LoadAttachments();
IncludedAttachments.Add(viewModel);
}
private async Task UpdateMimeChangesAsync() private async Task UpdateMimeChangesAsync()
{ {
if (isUpdatingMimeBlocked || CurrentMimeMessage == null || ComposingAccount == null || CurrentMailDraftItem == null) return; if (isUpdatingMimeBlocked || CurrentMimeMessage == null || ComposingAccount == null || CurrentMailDraftItem == null) return;
@@ -190,11 +219,13 @@ 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();
SaveSubject(); SaveSubject();
SaveFromAddress();
SaveReplyToAddress();
await SaveAttachmentsAsync(); await SaveAttachmentsAsync();
await SaveBodyAsync(); await SaveBodyAsync();
@@ -208,6 +239,8 @@ namespace Wino.Mail.ViewModels
{ {
CurrentMailDraftItem.Subject = CurrentMimeMessage.Subject; CurrentMailDraftItem.Subject = CurrentMimeMessage.Subject;
CurrentMailDraftItem.PreviewText = CurrentMimeMessage.TextBody; CurrentMailDraftItem.PreviewText = CurrentMimeMessage.TextBody;
CurrentMailDraftItem.FromAddress = SelectedAlias.AliasAddress;
CurrentMailDraftItem.HasAttachments = CurrentMimeMessage.Attachments.Any();
// Update database. // Update database.
await _mailService.UpdateMailAsync(CurrentMailDraftItem.MailCopy); await _mailService.UpdateMailAsync(CurrentMailDraftItem.MailCopy);
@@ -225,7 +258,10 @@ namespace Wino.Mail.ViewModels
} }
} }
private void SaveImportance() { CurrentMimeMessage.Importance = IsImportanceSelected ? SelectedMessageImportance : MessageImportance.Normal; } private void SaveImportance()
{
CurrentMimeMessage.Importance = IsImportanceSelected ? SelectedMessageImportance : MessageImportance.Normal;
}
private void SaveSubject() private void SaveSubject()
{ {
@@ -235,22 +271,38 @@ namespace Wino.Mail.ViewModels
} }
} }
private void ClearCurrentMimeAttachments()
{
var attachments = new List<MimePart>();
var multiparts = new List<Multipart>();
var iter = new MimeIterator(CurrentMimeMessage);
// collect our list of attachments and their parent multiparts
while (iter.MoveNext())
{
var multipart = iter.Parent as Multipart;
var part = iter.Current as MimePart;
if (multipart != null && part != null && part.IsAttachment)
{
// keep track of each attachment's parent multipart
multiparts.Add(multipart);
attachments.Add(part);
}
}
// now remove each attachment from its parent multipart...
for (int i = 0; i < attachments.Count; i++)
multiparts[i].Remove(attachments[i]);
}
private async Task SaveBodyAsync() private async Task SaveBodyAsync()
{ {
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)
CurrentMimeMessage.Body = bodyBuilder.ToMessageBody(); CurrentMimeMessage.Body = bodyBuilder.ToMessageBody();
} }
@@ -289,6 +341,8 @@ namespace Wino.Mail.ViewModels
base.OnNavigatedFrom(mode, parameters); base.OnNavigatedFrom(mode, parameters);
await UpdateMimeChangesAsync().ConfigureAwait(false); await UpdateMimeChangesAsync().ConfigureAwait(false);
Messenger.Send(new KillChromiumRequested());
} }
public override async void OnNavigatedTo(NavigationMode mode, object parameters) public override async void OnNavigatedTo(NavigationMode mode, object parameters)
@@ -297,92 +351,58 @@ namespace Wino.Mail.ViewModels
if (parameters != null && parameters is MailItemViewModel mailItem) if (parameters != null && parameters is MailItemViewModel mailItem)
{ {
await LoadAccountsAsync();
CurrentMailDraftItem = mailItem; CurrentMailDraftItem = mailItem;
_ = TryPrepareComposeAsync(true); await TryPrepareComposeAsync(true);
}
ToItems.CollectionChanged -= ContactListCollectionChanged;
ToItems.CollectionChanged += ContactListCollectionChanged;
// Check if there is any delivering mail address from protocol launch.
if (_launchProtocolService.MailtoParameters != null)
{
// TODO
//var requestedMailContact = await GetAddressInformationAsync(_launchProtocolService.MailtoParameters, ToItems);
//if (requestedMailContact != null)
//{
// ToItems.Add(requestedMailContact);
//}
//else
// DialogService.InfoBarMessage("Invalid Address", "Address is not a valid e-mail address.", InfoBarMessageType.Warning);
// Clear the address.
_launchProtocolService.MailtoParameters = null;
}
}
private void ContactListCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
// Prevent duplicates.
if (!(sender is ObservableCollection<AddressInformation> list))
return;
foreach (var item in e.NewItems)
{
if (item is AddressInformation addedInfo && list.Count(a => a == addedInfo) > 1)
{
var addedIndex = list.IndexOf(addedInfo);
list.RemoveAt(addedIndex);
}
}
}
}
private async Task LoadAccountsAsync()
{
// Load accounts
var accounts = await _accountService.GetAccountsAsync();
foreach (var account in accounts)
{
Accounts.Add(account);
} }
} }
private async Task<bool> InitializeComposerAccountAsync() private async Task<bool> InitializeComposerAccountAsync()
{ {
if (CurrentMailDraftItem == null) return false;
if (ComposingAccount != null) return true; if (ComposingAccount != null) return true;
if (CurrentMailDraftItem == null) var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.AssignedAccount.Id).ConfigureAwait(false);
return false; if (composingAccount == null) return false;
var aliases = await _accountService.GetAccountAliasesAsync(composingAccount.Id).ConfigureAwait(false);
if (aliases == null || !aliases.Any()) return false;
// MailAccountAlias primaryAlias = aliases.Find(a => a.IsPrimary) ?? aliases.First();
// Auto-select the correct alias from the message itself.
// If can't, fallback to primary alias.
MailAccountAlias primaryAlias = null;
if (!string.IsNullOrEmpty(CurrentMailDraftItem.FromAddress))
{
primaryAlias = aliases.Find(a => a.AliasAddress == CurrentMailDraftItem.FromAddress);
}
primaryAlias ??= await _accountService.GetPrimaryAccountAliasAsync(ComposingAccount.Id).ConfigureAwait(false);
await ExecuteUIThread(() => await ExecuteUIThread(() =>
{ {
ComposingAccount = Accounts.FirstOrDefault(a => a.Id == CurrentMailDraftItem.AssignedAccount.Id); ComposingAccount = composingAccount;
AvailableAliases = aliases;
SelectedAlias = primaryAlias;
}); });
return ComposingAccount != null; return true;
} }
private async Task TryPrepareComposeAsync(bool downloadIfNeeded) private async Task TryPrepareComposeAsync(bool downloadIfNeeded)
{ {
if (CurrentMailDraftItem == null) if (CurrentMailDraftItem == null) return;
return;
bool isComposerInitialized = await InitializeComposerAccountAsync(); bool isComposerInitialized = await InitializeComposerAccountAsync();
if (!isComposerInitialized) if (!isComposerInitialized) return;
{
return; retry:
}
// Replying existing message. // Replying existing message.
MimeMessageInformation mimeMessageInformation = null; MimeMessageInformation mimeMessageInformation = null;
@@ -395,20 +415,25 @@ namespace Wino.Mail.ViewModels
{ {
if (downloadIfNeeded) if (downloadIfNeeded)
{ {
// TODO: Folder id needs to be passed. downloadIfNeeded = false;
// TODO: Send mail retrieve request.
// _worker.Queue(new FetchSingleItemRequest(ComposingAccount.Id, CurrentMailDraftItem.Id, string.Empty)); var package = new DownloadMissingMessageRequested(CurrentMailDraftItem.AssignedAccount.Id, CurrentMailDraftItem.MailCopy);
var downloadResponse = await _winoServerConnectionManager.GetResponseAsync<bool, DownloadMissingMessageRequested>(package);
if (downloadResponse.IsSuccess)
{
goto retry;
} }
//else }
// DialogService.ShowMIMENotFoundMessage(); else
DialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error);
return; return;
} }
catch (IOException) catch (IOException)
{ {
DialogService.InfoBarMessage("Busy", "Mail is being processed. Please wait a moment and try again.", InfoBarMessageType.Warning); DialogService.InfoBarMessage(Translator.Busy, Translator.Exception_MailProcessing, 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);
@@ -426,27 +451,32 @@ namespace Wino.Mail.ViewModels
{ {
// Extract information // Extract information
CurrentMimeMessage = replyingMime;
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();
if (replyingMime.Cc.Any() || replyingMime.Bcc.Any())
IsCCBCCVisible = true;
Subject = replyingMime.Subject; Subject = replyingMime.Subject;
CurrentMimeMessage = replyingMime;
Messenger.Send(new CreateNewComposeMailRequested(renderModel)); Messenger.Send(new CreateNewComposeMailRequested(renderModel));
}); });
} }
private void LoadAttachments(IEnumerable<MimeEntity> mimeEntities) private void LoadAttachments()
{ {
foreach (var attachment in mimeEntities) if (CurrentMimeMessage == null) return;
foreach (var attachment in CurrentMimeMessage.Attachments)
{ {
if (attachment.IsAttachment && attachment is MimePart attachmentPart) if (attachment.IsAttachment && attachment is MimePart attachmentPart)
{ {
@@ -466,6 +496,28 @@ namespace Wino.Mail.ViewModels
} }
} }
private void SaveFromAddress()
{
if (SelectedAlias == null) return;
CurrentMimeMessage.From.Clear();
CurrentMimeMessage.From.Add(new MailboxAddress(ComposingAccount.SenderName, SelectedAlias.AliasAddress));
}
private void SaveReplyToAddress()
{
if (SelectedAlias == null) return;
if (!string.IsNullOrEmpty(SelectedAlias.ReplyToAddress))
{
if (!CurrentMimeMessage.ReplyTo.Any(a => a is MailboxAddress mailboxAddress && mailboxAddress.Address == SelectedAlias.ReplyToAddress))
{
CurrentMimeMessage.ReplyTo.Clear();
CurrentMimeMessage.ReplyTo.Add(new MailboxAddress(SelectedAlias.ReplyToAddress, SelectedAlias.ReplyToAddress));
}
}
}
private void SaveAddressInfo(IEnumerable<AddressInformation> addresses, InternetAddressList list) private void SaveAddressInfo(IEnumerable<AddressInformation> addresses, InternetAddressList list)
{ {
list.Clear(); list.Clear();

View File

@@ -23,6 +23,8 @@ namespace Wino.Mail.ViewModels.Data
public int HoldingAccountCount => 1; public int HoldingAccountCount => 1;
public bool HasProfilePicture => !string.IsNullOrEmpty(Account.Base64ProfilePictureData);
public AccountProviderDetailViewModel(IProviderDetail providerDetail, MailAccount account) public AccountProviderDetailViewModel(IProviderDetail providerDetail, MailAccount account)
{ {
ProviderDetail = providerDetail; ProviderDetail = providerDetail;

View File

@@ -3,31 +3,21 @@ using Wino.Messaging.Client.Navigation;
namespace Wino.Mail.ViewModels.Data namespace Wino.Mail.ViewModels.Data
{ {
public class BreadcrumbNavigationItemViewModel : ObservableObject public partial class BreadcrumbNavigationItemViewModel : ObservableObject
{ {
[ObservableProperty]
private string title;
[ObservableProperty]
private bool isActive;
public BreadcrumbNavigationRequested Request { get; set; } public BreadcrumbNavigationRequested Request { get; set; }
public BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive) public BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive)
{ {
Request = request; Request = request;
Title = request.PageTitle; Title = request.PageTitle;
IsActive = isActive;
this.isActive = isActive;
}
private string title;
public string Title
{
get => title;
set => SetProperty(ref title, value);
}
private bool isActive;
public bool IsActive
{
get => isActive;
set => SetProperty(ref isActive, value);
} }
} }
} }

View File

@@ -6,9 +6,8 @@ using Wino.Core.Extensions;
namespace Wino.Mail.ViewModels.Data namespace Wino.Mail.ViewModels.Data
{ {
public class MailAttachmentViewModel : ObservableObject public partial class MailAttachmentViewModel : ObservableObject
{ {
private bool isBusy;
private readonly MimePart _mimePart; private readonly MimePart _mimePart;
public MailAttachmentType AttachmentType { get; } public MailAttachmentType AttachmentType { get; }
@@ -22,23 +21,21 @@ namespace Wino.Mail.ViewModels.Data
/// <summary> /// <summary>
/// Gets or sets whether attachment is busy with opening or saving etc. /// Gets or sets whether attachment is busy with opening or saving etc.
/// </summary> /// </summary>
public bool IsBusy [ObservableProperty]
{ private bool isBusy;
get => isBusy;
set => SetProperty(ref isBusy, value);
}
public MailAttachmentViewModel(MimePart mimePart) public MailAttachmentViewModel(MimePart mimePart)
{ {
_mimePart = mimePart; _mimePart = mimePart;
var array = new byte[_mimePart.Content.Stream.Length]; var memoryStream = new MemoryStream();
_mimePart.Content.Stream.Read(array, 0, (int)_mimePart.Content.Stream.Length);
Content = array; using (memoryStream) mimePart.Content.DecodeTo(memoryStream);
Content = memoryStream.ToArray();
FileName = mimePart.FileName; FileName = mimePart.FileName;
ReadableSize = mimePart.Content.Stream.Length.GetBytesReadable(); ReadableSize = ((long)Content.Length).GetBytesReadable();
var extension = Path.GetExtension(FileName); var extension = Path.GetExtension(FileName);
AttachmentType = GetAttachmentType(extension); AttachmentType = GetAttachmentType(extension);

View File

@@ -18,8 +18,6 @@ namespace Wino.Mail.ViewModels.Data
public string MessageId => ((IMailItem)MailCopy).MessageId; public string MessageId => ((IMailItem)MailCopy).MessageId;
public string FromName => ((IMailItem)MailCopy).FromName ?? FromAddress; public string FromName => ((IMailItem)MailCopy).FromName ?? FromAddress;
public DateTime CreationDate => ((IMailItem)MailCopy).CreationDate; public DateTime CreationDate => ((IMailItem)MailCopy).CreationDate;
public string FromAddress => ((IMailItem)MailCopy).FromAddress;
public bool HasAttachments => ((IMailItem)MailCopy).HasAttachments;
public string References => ((IMailItem)MailCopy).References; public string References => ((IMailItem)MailCopy).References;
public string InReplyTo => ((IMailItem)MailCopy).InReplyTo; public string InReplyTo => ((IMailItem)MailCopy).InReplyTo;
@@ -77,6 +75,18 @@ namespace Wino.Mail.ViewModels.Data
set => SetProperty(MailCopy.PreviewText, value, MailCopy, (u, n) => u.PreviewText = n); set => SetProperty(MailCopy.PreviewText, value, MailCopy, (u, n) => u.PreviewText = n);
} }
public string FromAddress
{
get => MailCopy.FromAddress;
set => SetProperty(MailCopy.FromAddress, value, MailCopy, (u, n) => u.FromAddress = n);
}
public bool HasAttachments
{
get => MailCopy.HasAttachments;
set => SetProperty(MailCopy.HasAttachments, value, MailCopy, (u, n) => u.HasAttachments = n);
}
public MailItemFolder AssignedFolder => ((IMailItem)MailCopy).AssignedFolder; public MailItemFolder AssignedFolder => ((IMailItem)MailCopy).AssignedFolder;
public MailAccount AssignedAccount => ((IMailItem)MailCopy).AssignedAccount; public MailAccount AssignedAccount => ((IMailItem)MailCopy).AssignedAccount;
@@ -94,6 +104,8 @@ namespace Wino.Mail.ViewModels.Data
OnPropertyChanged(nameof(DraftId)); OnPropertyChanged(nameof(DraftId));
OnPropertyChanged(nameof(Subject)); OnPropertyChanged(nameof(Subject));
OnPropertyChanged(nameof(PreviewText)); OnPropertyChanged(nameof(PreviewText));
OnPropertyChanged(nameof(FromAddress));
OnPropertyChanged(nameof(HasAttachments));
} }
public IEnumerable<Guid> GetContainingIds() => new[] { UniqueId }; public IEnumerable<Guid> GetContainingIds() => new[] { UniqueId };

View File

@@ -68,7 +68,7 @@ namespace Wino.Mail.ViewModels
private CancellationTokenSource listManipulationCancellationTokenSource = new CancellationTokenSource(); private CancellationTokenSource listManipulationCancellationTokenSource = new CancellationTokenSource();
public IWinoNavigationService NavigationService { get; } public IWinoNavigationService NavigationService { get; }
public IStatePersistanceService StatePersistanceService { get; } public IStatePersistanceService StatePersistenceService { get; }
public IPreferencesService PreferencesService { get; } public IPreferencesService PreferencesService { get; }
private readonly IMailService _mailService; private readonly IMailService _mailService;
@@ -120,6 +120,12 @@ namespace Wino.Mail.ViewModels
[ObservableProperty] [ObservableProperty]
private string barMessage; private string barMessage;
[ObservableProperty]
private double mailListLength = 420;
[ObservableProperty]
private double maxMailListLength = 1200;
[ObservableProperty] [ObservableProperty]
private string barTitle; private string barTitle;
@@ -141,7 +147,7 @@ namespace Wino.Mail.ViewModels
public MailListPageViewModel(IDialogService dialogService, public MailListPageViewModel(IDialogService dialogService,
IWinoNavigationService navigationService, IWinoNavigationService navigationService,
IMailService mailService, IMailService mailService,
IStatePersistanceService statePersistanceService, IStatePersistanceService statePersistenceService,
IFolderService folderService, IFolderService folderService,
IThreadingStrategyProvider threadingStrategyProvider, IThreadingStrategyProvider threadingStrategyProvider,
IContextMenuItemService contextMenuItemService, IContextMenuItemService contextMenuItemService,
@@ -152,7 +158,7 @@ namespace Wino.Mail.ViewModels
{ {
PreferencesService = preferencesService; PreferencesService = preferencesService;
_winoServerConnectionManager = winoServerConnectionManager; _winoServerConnectionManager = winoServerConnectionManager;
StatePersistanceService = statePersistanceService; StatePersistenceService = statePersistenceService;
NavigationService = navigationService; NavigationService = navigationService;
_mailService = mailService; _mailService = mailService;
@@ -165,6 +171,8 @@ namespace Wino.Mail.ViewModels
SelectedFilterOption = FilterOptions[0]; SelectedFilterOption = FilterOptions[0];
SelectedSortingOption = SortingOptions[0]; SelectedSortingOption = SortingOptions[0];
mailListLength = statePersistenceService.MailListPaneLength;
selectionChangedObservable = Observable.FromEventPattern<NotifyCollectionChangedEventArgs>(SelectedItems, nameof(SelectedItems.CollectionChanged)); selectionChangedObservable = Observable.FromEventPattern<NotifyCollectionChangedEventArgs>(SelectedItems, nameof(SelectedItems.CollectionChanged));
selectionChangedObservable selectionChangedObservable
.Throttle(TimeSpan.FromMilliseconds(100)) .Throttle(TimeSpan.FromMilliseconds(100))
@@ -257,7 +265,7 @@ namespace Wino.Mail.ViewModels
{ {
if (_activeMailItem == selectedMailItemViewModel) return; if (_activeMailItem == selectedMailItemViewModel) return;
// Don't update active mail item if Ctrl key is pressed or multi selection is ennabled. // Don't update active mail item if Ctrl key is pressed or multi selection is enabled.
// User is probably trying to select multiple items. // User is probably trying to select multiple items.
// This is not the same behavior in Windows Mail, // This is not the same behavior in Windows Mail,
// but it's a trash behavior. // but it's a trash behavior.
@@ -266,7 +274,7 @@ namespace Wino.Mail.ViewModels
bool isMultiSelecting = isCtrlKeyPressed || IsMultiSelectionModeEnabled; bool isMultiSelecting = isCtrlKeyPressed || IsMultiSelectionModeEnabled;
if (isMultiSelecting ? StatePersistanceService.IsReaderNarrowed : false) if (isMultiSelecting && StatePersistenceService.IsReaderNarrowed)
{ {
// Don't change the active mail item if the reader is narrowed, but just update the shell. // Don't change the active mail item if the reader is narrowed, but just update the shell.
Messenger.Send(new ShellStateUpdated()); Messenger.Send(new ShellStateUpdated());
@@ -610,6 +618,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 +628,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 +857,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 +874,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, draftOptions.Reason, 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

@@ -25,37 +25,24 @@ namespace Wino.Mail.ViewModels
public bool IsSelectedWindowsAccentColor => SelectedAppColor == Colors.LastOrDefault(); public bool IsSelectedWindowsAccentColor => SelectedAppColor == Colors.LastOrDefault();
public ObservableCollection<AppColorViewModel> Colors { get; set; } = new ObservableCollection<AppColorViewModel>(); public ObservableCollection<AppColorViewModel> Colors { get; set; } = [];
public List<ElementThemeContainer> ElementThemes { get; set; } = new List<ElementThemeContainer>() public List<ElementThemeContainer> ElementThemes { get; set; } =
{ [
new ElementThemeContainer(ApplicationElementTheme.Light, Translator.ElementTheme_Light), new ElementThemeContainer(ApplicationElementTheme.Light, Translator.ElementTheme_Light),
new ElementThemeContainer(ApplicationElementTheme.Dark, Translator.ElementTheme_Dark), new ElementThemeContainer(ApplicationElementTheme.Dark, Translator.ElementTheme_Dark),
new ElementThemeContainer(ApplicationElementTheme.Default, Translator.ElementTheme_Default), new ElementThemeContainer(ApplicationElementTheme.Default, Translator.ElementTheme_Default),
}; ];
public List<MailListPaneLengthPreferences> PaneLengths { get; set; } = new List<MailListPaneLengthPreferences>() public List<MailListDisplayMode> InformationDisplayModes { get; set; } =
{ [
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Micro, 300),
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Small, 350),
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Default, 420),
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Medium, 700),
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Large, 900),
new MailListPaneLengthPreferences(Translator.PaneLengthOption_ExtraLarge, 1200),
};
public List<MailListDisplayMode> InformationDisplayModes { get; set; } = new List<MailListDisplayMode>()
{
MailListDisplayMode.Compact, MailListDisplayMode.Compact,
MailListDisplayMode.Medium, MailListDisplayMode.Medium,
MailListDisplayMode.Spacious MailListDisplayMode.Spacious
}; ];
public List<AppThemeBase> AppThemes { get; set; } public List<AppThemeBase> AppThemes { get; set; }
[ObservableProperty]
private MailListPaneLengthPreferences selectedMailListPaneLength;
[ObservableProperty] [ObservableProperty]
private ElementThemeContainer selectedElementTheme; private ElementThemeContainer selectedElementTheme;
@@ -123,6 +110,13 @@ namespace Wino.Mail.ViewModels
#endregion #endregion
[RelayCommand]
private void ResetMailListPaneLength()
{
StatePersistanceService.MailListPaneLength = 420;
DialogService.InfoBarMessage(Translator.GeneralTitle_Info, Translator.Info_MailListSizeResetSuccessMessage, InfoBarMessageType.Success);
}
public AsyncRelayCommand CreateCustomThemeCommand { get; set; } public AsyncRelayCommand CreateCustomThemeCommand { get; set; }
public PersonalizationPageViewModel(IDialogService dialogService, public PersonalizationPageViewModel(IDialogService dialogService,
IStatePersistanceService statePersistanceService, IStatePersistanceService statePersistanceService,
@@ -179,7 +173,6 @@ namespace Wino.Mail.ViewModels
{ {
SelectedElementTheme = ElementThemes.Find(a => a.NativeTheme == _themeService.RootTheme); SelectedElementTheme = ElementThemes.Find(a => a.NativeTheme == _themeService.RootTheme);
SelectedInfoDisplayMode = PreferencesService.MailItemDisplayMode; SelectedInfoDisplayMode = PreferencesService.MailItemDisplayMode;
SelectedMailListPaneLength = PaneLengths.Find(a => a.Length == StatePersistanceService.MailListPaneLength);
var currentAccentColor = _themeService.AccentColor; var currentAccentColor = _themeService.AccentColor;
@@ -289,8 +282,6 @@ namespace Wino.Mail.ViewModels
{ {
_themeService.CurrentApplicationThemeId = SelectedAppTheme.Id; _themeService.CurrentApplicationThemeId = SelectedAppTheme.Id;
} }
else if (e.PropertyName == nameof(SelectedMailListPaneLength) && SelectedMailListPaneLength != null)
StatePersistanceService.MailListPaneLength = SelectedMailListPaneLength.Length;
else else
{ {
if (e.PropertyName == nameof(SelectedInfoDisplayMode)) if (e.PropertyName == nameof(SelectedInfoDisplayMode))

View File

@@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="EmailValidation" Version="1.2.0" />
<PackageReference Include="IsExternalInit" Version="1.0.3"> <PackageReference Include="IsExternalInit" Version="1.0.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

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

@@ -1,4 +1,5 @@
<Application x:Class="Wino.App" <Application
x:Class="Wino.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Wino.Controls" xmlns:controls="using:Wino.Controls"
@@ -48,6 +49,14 @@
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
</Style> </Style>
<!-- Custom Grid style for info panels. -->
<Style TargetType="Grid" x:Key="InformationAreaGridStyle">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="16" />
</Style>
<!-- Default StackPanel animation. --> <!-- Default StackPanel animation. -->
<Style TargetType="StackPanel"> <Style TargetType="StackPanel">
<Setter Property="ChildrenTransitions"> <Setter Property="ChildrenTransitions">
@@ -60,7 +69,8 @@
</Style> </Style>
<!-- Default Style for ContentDialog --> <!-- Default Style for ContentDialog -->
<Style x:Key="WinoDialogStyle" <Style
x:Key="WinoDialogStyle"
BasedOn="{StaticResource DefaultContentDialogStyle}" BasedOn="{StaticResource DefaultContentDialogStyle}"
TargetType="ContentDialog" /> TargetType="ContentDialog" />
@@ -74,14 +84,16 @@
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="controls:SettingsMenuItemControl"> <ControlTemplate TargetType="controls:SettingsMenuItemControl">
<Grid> <Grid>
<Button Padding="0" <Button
Padding="0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
Command="{TemplateBinding Command}" Command="{TemplateBinding Command}"
CommandParameter="{TemplateBinding CommandParameter}" CommandParameter="{TemplateBinding CommandParameter}"
IsEnabled="{TemplateBinding IsEnabled}" IsEnabled="{TemplateBinding IsEnabled}"
IsHitTestVisible="{TemplateBinding IsClickable}"> IsHitTestVisible="{TemplateBinding IsClickable}">
<Grid Height="70" <Grid
Height="70"
Padding="0,6,12,6" Padding="0,6,12,6"
CornerRadius="4"> CornerRadius="4">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -89,11 +101,13 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ContentControl HorizontalAlignment="Center" <ContentControl
HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Content="{TemplateBinding Icon}" /> Content="{TemplateBinding Icon}" />
<Grid Grid.Column="1" <Grid
Grid.Column="1"
Margin="4,0" Margin="4,0"
VerticalAlignment="Center" VerticalAlignment="Center"
RowSpacing="3"> RowSpacing="3">
@@ -102,17 +116,20 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock VerticalAlignment="Center" <TextBlock
VerticalAlignment="Center"
FontWeight="SemiBold" FontWeight="SemiBold"
Style="{StaticResource BodyTextBlockStyle}" Style="{StaticResource BodyTextBlockStyle}"
Text="{TemplateBinding Title}" /> Text="{TemplateBinding Title}" />
<TextBlock Grid.Row="1" <TextBlock
Grid.Row="1"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" Style="{StaticResource CaptionTextBlockStyle}"
Text="{TemplateBinding Description}" /> Text="{TemplateBinding Description}" />
</Grid> </Grid>
<Viewbox Grid.Column="0" <Viewbox
Grid.Column="0"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Width="16" Width="16"
Height="16" Height="16"
@@ -124,7 +141,8 @@
</Grid> </Grid>
</Button> </Button>
<ContentControl Grid.RowSpan="2" <ContentControl
Grid.RowSpan="2"
Margin="0,0,16,0" Margin="0,0,16,0"
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -188,7 +206,8 @@
<Image Source="/Assets/FileTypes/type_other.png" /> <Image Source="/Assets/FileTypes/type_other.png" />
</DataTemplate> </DataTemplate>
<selectors:FileAttachmentTypeSelector x:Key="FileTypeIconSelector" <selectors:FileAttachmentTypeSelector
x:Key="FileTypeIconSelector"
Archive="{StaticResource ArchiveTemplate}" Archive="{StaticResource ArchiveTemplate}"
Executable="{StaticResource ExecutableTemplate}" Executable="{StaticResource ExecutableTemplate}"
HTML="{StaticResource HTMLTemplate}" HTML="{StaticResource HTMLTemplate}"

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>();
} }
@@ -184,6 +179,7 @@ namespace Wino
services.AddTransient(typeof(MergedAccountDetailsPageViewModel)); services.AddTransient(typeof(MergedAccountDetailsPageViewModel));
services.AddTransient(typeof(LanguageTimePageViewModel)); services.AddTransient(typeof(LanguageTimePageViewModel));
services.AddTransient(typeof(AppPreferencesPageViewModel)); services.AddTransient(typeof(AppPreferencesPageViewModel));
services.AddTransient(typeof(AliasManagementPageViewModel));
} }
#endregion #endregion
@@ -283,19 +279,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 +359,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 +413,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

@@ -208,7 +208,6 @@ namespace Wino.Views
WeakReferenceMessenger.Default.Send(new ClearMailSelectionsRequested()); WeakReferenceMessenger.Default.Send(new ClearMailSelectionsRequested());
WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested()); WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested());
WeakReferenceMessenger.Default.Send(new ShellStateUpdated()); WeakReferenceMessenger.Default.Send(new ShellStateUpdated());
} }
private async void MenuItemContextRequested(UIElement sender, ContextRequestedEventArgs args) private async void MenuItemContextRequested(UIElement sender, ContextRequestedEventArgs args)

View File

@@ -1,4 +1,5 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:xaml="using:Windows.UI.Xaml"> xmlns:xaml="using:Windows.UI.Xaml">

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

@@ -0,0 +1,27 @@
using System;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml;
namespace Wino.Converters
{
public class GridLengthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is double doubleValue)
{
return new GridLength(doubleValue);
}
return new GridLength(1, GridUnitType.Auto);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
if (value is GridLength gridLength)
{
return gridLength.Value;
}
return 0.0;
}
}
}

View File

@@ -47,6 +47,13 @@
</VisualState.Setters> </VisualState.Setters>
</VisualState> </VisualState>
<VisualState x:Name="FetchingProfileInformation">
<VisualState.Setters>
<Setter Target="StatusText.Text" Value="{x:Bind domain:Translator.AccountCreationDialog_FetchingProfileInformation}" />
<Setter Target="DialogIcon.Glyph" Value="F1 M 1.875 17.5 C 1.621094 17.5 1.380208 17.451172 1.152344 17.353516 C 0.924479 17.255859 0.724284 17.120768 0.551758 16.948242 C 0.379232 16.775717 0.244141 16.575521 0.146484 16.347656 C 0.048828 16.119791 0 15.878906 0 15.625 L 0 4.375 C 0 4.121094 0.048828 3.880209 0.146484 3.652344 C 0.244141 3.42448 0.379232 3.224285 0.551758 3.051758 C 0.724284 2.879232 0.924479 2.744141 1.152344 2.646484 C 1.380208 2.548828 1.621094 2.5 1.875 2.5 L 18.125 2.5 C 18.378906 2.5 18.619791 2.548828 18.847656 2.646484 C 19.07552 2.744141 19.275715 2.879232 19.448242 3.051758 C 19.620768 3.224285 19.755859 3.42448 19.853516 3.652344 C 19.951172 3.880209 20 4.121094 20 4.375 L 20 15.625 C 20 15.878906 19.951172 16.119791 19.853516 16.347656 C 19.755859 16.575521 19.620768 16.775717 19.448242 16.948242 C 19.275715 17.120768 19.07552 17.255859 18.847656 17.353516 C 18.619791 17.451172 18.378906 17.5 18.125 17.5 Z M 5 16.25 L 5 14.375 C 5 14.121094 5.048828 13.880209 5.146484 13.652344 C 5.244141 13.424479 5.379231 13.224284 5.551758 13.051758 C 5.724284 12.879232 5.924479 12.744141 6.152344 12.646484 C 6.380208 12.548828 6.621094 12.5 6.875 12.5 L 13.125 12.5 C 13.378905 12.5 13.619791 12.548828 13.847656 12.646484 C 14.075521 12.744141 14.275716 12.879232 14.448242 13.051758 C 14.620768 13.224284 14.755858 13.424479 14.853516 13.652344 C 14.951171 13.880209 14.999999 14.121094 15 14.375 L 15 16.25 L 18.125 16.25 C 18.29427 16.25 18.440754 16.188152 18.564453 16.064453 C 18.68815 15.940756 18.75 15.794271 18.75 15.625 L 18.75 4.375 C 18.75 4.20573 18.68815 4.059246 18.564453 3.935547 C 18.440754 3.81185 18.29427 3.75 18.125 3.75 L 1.875 3.75 C 1.705729 3.75 1.559245 3.81185 1.435547 3.935547 C 1.311849 4.059246 1.25 4.20573 1.25 4.375 L 1.25 15.625 C 1.25 15.794271 1.311849 15.940756 1.435547 16.064453 C 1.559245 16.188152 1.705729 16.25 1.875 16.25 Z M 6.875 8.056641 C 6.875 7.633464 6.959635 7.236328 7.128906 6.865234 C 7.298177 6.494141 7.526041 6.170248 7.8125 5.893555 C 8.098958 5.616862 8.430989 5.398764 8.808594 5.239258 C 9.186197 5.079754 9.583333 5.000001 10 5 C 10.436197 5.000001 10.843099 5.081381 11.220703 5.244141 C 11.598307 5.406901 11.928711 5.629883 12.211914 5.913086 C 12.495117 6.196289 12.718099 6.526693 12.880859 6.904297 C 13.043619 7.281901 13.124999 7.688803 13.125 8.125 C 13.124999 8.561198 13.043619 8.9681 12.880859 9.345703 C 12.718099 9.723308 12.495117 10.053711 12.211914 10.336914 C 11.928711 10.620117 11.598307 10.8431 11.220703 11.005859 C 10.843099 11.16862 10.436197 11.25 10 11.25 C 9.550781 11.25 9.135742 11.166992 8.754883 11.000977 C 8.374023 10.834961 8.043619 10.607097 7.763672 10.317383 C 7.483724 10.02767 7.265625 9.689128 7.109375 9.301758 C 6.953125 8.914389 6.875 8.49935 6.875 8.056641 Z M 11.875 8.125 C 11.875 7.871094 11.826172 7.630209 11.728516 7.402344 C 11.630859 7.174479 11.495768 6.974284 11.323242 6.801758 C 11.150716 6.629232 10.950521 6.494141 10.722656 6.396484 C 10.494791 6.298828 10.253906 6.25 10 6.25 C 9.746094 6.25 9.505208 6.298828 9.277344 6.396484 C 9.049479 6.494141 8.849283 6.629232 8.676758 6.801758 C 8.504231 6.974284 8.369141 7.174479 8.271484 7.402344 C 8.173828 7.630209 8.125 7.871094 8.125 8.125 C 8.125 8.378906 8.173828 8.619792 8.271484 8.847656 C 8.369141 9.075521 8.504231 9.275717 8.676758 9.448242 C 8.849283 9.620769 9.049479 9.755859 9.277344 9.853516 C 9.505208 9.951172 9.746094 10 10 10 C 10.253906 10 10.494791 9.951172 10.722656 9.853516 C 10.950521 9.755859 11.150716 9.620769 11.323242 9.448242 C 11.495768 9.275717 11.630859 9.075521 11.728516 8.847656 C 11.826172 8.619792 11.875 8.378906 11.875 8.125 Z M 6.25 16.25 L 13.75 16.25 L 13.75 14.375 C 13.75 14.205729 13.68815 14.059245 13.564453 13.935547 C 13.440755 13.81185 13.294271 13.75 13.125 13.75 L 6.875 13.75 C 6.705729 13.75 6.559244 13.81185 6.435547 13.935547 C 6.311849 14.059245 6.25 14.205729 6.25 14.375 Z " />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Completed"> <VisualState x:Name="Completed">
<VisualState.Setters> <VisualState.Setters>
<Setter Target="StatusText.Text" Value="{x:Bind domain:Translator.AccountCreationDialog_Completed}" /> <Setter Target="StatusText.Text" Value="{x:Bind domain:Translator.AccountCreationDialog_Completed}" />

View File

@@ -0,0 +1,40 @@
<ContentDialog
x:Class="Wino.Dialogs.CreateAccountAliasDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Wino.Dialogs"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:domain="using:Wino.Core.Domain"
PrimaryButtonText="{x:Bind domain:Translator.Buttons_Create}"
SecondaryButtonText="{x:Bind domain:Translator.Buttons_Cancel}"
DefaultButton="Primary"
PrimaryButtonClick="CreateClicked"
Title="{x:Bind domain:Translator.CreateAccountAliasDialog_Title}"
Style="{StaticResource WinoDialogStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="{x:Bind domain:Translator.CreateAccountAliasDialog_Description}" Style="{StaticResource CaptionTextBlockStyle}" />
<StackPanel
Grid.Row="1"
Margin="0,20"
Spacing="8">
<TextBox
x:Name="AliasTextBox"
PlaceholderText="{x:Bind domain:Translator.CreateAccountAliasDialog_AliasAddressPlaceholder}"
Header="{x:Bind domain:Translator.CreateAccountAliasDialog_AliasAddress}" />
<TextBox
x:Name="ReplyToTextBox"
PlaceholderText="{x:Bind domain:Translator.CreateAccountAliasDialog_ReplyToAddressPlaceholder}"
Header="{x:Bind domain:Translator.CreateAccountAliasDialog_ReplyToAddress}" />
</StackPanel>
</Grid>
</ContentDialog>

View File

@@ -0,0 +1,30 @@
using System;
using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Interfaces;
namespace Wino.Dialogs
{
public sealed partial class CreateAccountAliasDialog : ContentDialog, ICreateAccountAliasDialog
{
public MailAccountAlias CreatedAccountAlias { get; set; }
public CreateAccountAliasDialog()
{
InitializeComponent();
}
private void CreateClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
CreatedAccountAlias = new MailAccountAlias
{
AliasAddress = AliasTextBox.Text.Trim(),
ReplyToAddress = ReplyToTextBox.Text.Trim(),
Id = Guid.NewGuid(),
IsPrimary = false,
IsVerified = false
};
Hide();
}
}
}

View File

@@ -56,14 +56,6 @@
PlaceholderText="{x:Bind domain:Translator.NewAccountDialog_AccountNamePlaceholder}" PlaceholderText="{x:Bind domain:Translator.NewAccountDialog_AccountNamePlaceholder}"
TextChanged="AccountNameChanged" /> TextChanged="AccountNameChanged" />
<!-- Sender Name -->
<TextBox
x:Name="SenderNameTextbox"
Grid.Row="1"
Header="{x:Bind domain:Translator.AccountSettingsDialog_AccountName}"
PlaceholderText="{x:Bind domain:Translator.AccountSettingsDialog_AccountNamePlaceholder}"
TextChanged="SenderNameChanged" />
<!-- <!--
TODO: Move Name, Sender Name and Color Picker to another Frame. TODO: Move Name, Sender Name and Color Picker to another Frame.
Provider selection should be first, then account details. Provider selection should be first, then account details.
@@ -79,7 +71,7 @@
<ListView <ListView
Grid.Row="2" Grid.Row="2"
Margin="0,12"
Padding="0" Padding="0"
ItemTemplate="{StaticResource NewMailProviderTemplate}" ItemTemplate="{StaticResource NewMailProviderTemplate}"
ItemsSource="{x:Bind Providers}" ItemsSource="{x:Bind Providers}"

View File

@@ -49,7 +49,7 @@ namespace Wino.Dialogs
if (IsSecondaryButtonEnabled) if (IsSecondaryButtonEnabled)
{ {
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim(), SenderNameTextbox.Text.Trim()); Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim());
Hide(); Hide();
} }
} }
@@ -68,8 +68,7 @@ namespace Wino.Dialogs
{ {
bool shouldEnable = SelectedMailProvider != null bool shouldEnable = SelectedMailProvider != null
&& SelectedMailProvider.IsSupported && SelectedMailProvider.IsSupported
&& !string.IsNullOrEmpty(AccountNameTextbox.Text) && !string.IsNullOrEmpty(AccountNameTextbox.Text);
&& (SelectedMailProvider.RequireSenderNameOnCreationDialog ? !string.IsNullOrEmpty(SenderNameTextbox.Text) : true);
IsPrimaryButtonEnabled = shouldEnable; IsPrimaryButtonEnabled = shouldEnable;
} }
@@ -77,7 +76,6 @@ namespace Wino.Dialogs
private void ValidateNames() private void ValidateNames()
{ {
AccountNameTextbox.IsEnabled = SelectedMailProvider != null; AccountNameTextbox.IsEnabled = SelectedMailProvider != null;
SenderNameTextbox.IsEnabled = SelectedMailProvider != null && SelectedMailProvider.Type != Core.Domain.Enums.MailProviderType.IMAP4;
} }
private void DialogOpened(ContentDialog sender, ContentDialogOpenedEventArgs args) => Validate(); private void DialogOpened(ContentDialog sender, ContentDialogOpenedEventArgs args) => Validate();

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

@@ -216,6 +216,18 @@ namespace Wino.Services
return storeDialog; return storeDialog;
} }
public async Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync()
{
var createAccountAliasDialog = new CreateAccountAliasDialog()
{
RequestedTheme = _themeService.RootTheme.ToWindowsElementTheme()
};
await HandleDialogPresentationAsync(createAccountAliasDialog);
return createAccountAliasDialog;
}
public async Task HandleSystemFolderConfigurationDialogAsync(Guid accountId, IFolderService folderService) public async Task HandleSystemFolderConfigurationDialogAsync(Guid accountId, IFolderService folderService)
{ {
try try

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; }
}
}

Some files were not shown because too many files have changed in this diff Show More