Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d623129d56 | ||
|
|
9cc4c33bb1 | ||
|
|
c087b40d4a | ||
|
|
a82e074bd4 | ||
|
|
3365c099bb | ||
|
|
d8705de26f | ||
|
|
3af181e736 | ||
|
|
ba6c01b7c6 | ||
|
|
7a7cdcb041 | ||
|
|
09e52bf199 | ||
|
|
a8c39a1587 | ||
|
|
68536d6c34 | ||
|
|
f57c27e755 | ||
|
|
9a97a27c8a | ||
|
|
07bb90dda9 | ||
|
|
3bb156f4da | ||
|
|
e13e0efcc6 | ||
|
|
3ae0a94159 | ||
|
|
eec67ec7dc | ||
|
|
cf51853eec | ||
|
|
67838b28a4 | ||
|
|
bf68e3b7d5 | ||
|
|
91ed0bb8bd | ||
|
|
55fe791c2a | ||
|
|
747efac2ec | ||
|
|
a87df2e9f6 | ||
|
|
2e4a664744 | ||
|
|
579a22ea45 | ||
|
|
abff850427 | ||
|
|
f1154058ba | ||
|
|
cf9f308b7f | ||
|
|
1791df236c | ||
|
|
7211f94f08 | ||
|
|
7b0343c87f | ||
|
|
b80f0276b4 | ||
|
|
8f66fcbb00 | ||
|
|
fe449ee1f3 | ||
|
|
34d6d95186 | ||
|
|
05ddc0660a | ||
|
|
c6047a8428 | ||
|
|
bc4838578e | ||
|
|
548996405a | ||
|
|
a9a5f0bd14 | ||
|
|
ec05ff6123 | ||
|
|
10c7ab421b | ||
|
|
a8a5cc53ea | ||
|
|
8fe48ca438 | ||
|
|
cbd5a515a9 | ||
|
|
5912adff93 | ||
|
|
983bc21448 | ||
|
|
6d08368462 | ||
|
|
cde7bb3524 | ||
|
|
133dc91561 | ||
|
|
f408f59beb | ||
|
|
8763bf11ab | ||
|
|
99592a52be | ||
|
|
25a8a52573 | ||
|
|
5901344459 |
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
Wino.Core.Domain/Entities/MailAccountAlias.cs
Normal file
56
Wino.Core.Domain/Entities/MailAccountAlias.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
ManuelSetupWaiting,
|
ManuelSetupWaiting,
|
||||||
TestingConnection,
|
TestingConnection,
|
||||||
AutoDiscoverySetup,
|
AutoDiscoverySetup,
|
||||||
AutoDiscoveryInProgress
|
AutoDiscoveryInProgress,
|
||||||
|
FetchingProfileInformation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,5 +23,6 @@
|
|||||||
LanguageTimePage,
|
LanguageTimePage,
|
||||||
AppPreferencesPage,
|
AppPreferencesPage,
|
||||||
SettingOptionsPage,
|
SettingOptionsPage,
|
||||||
|
AliasManagementPage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
Wino.Core.Domain/Exceptions/MissingAliasException.cs
Normal file
7
Wino.Core.Domain/Exceptions/MissingAliasException.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Wino.Core.Domain.Exceptions
|
||||||
|
{
|
||||||
|
public class MissingAliasException : System.Exception
|
||||||
|
{
|
||||||
|
public MissingAliasException() : base(Translator.Exception_MissingAlias) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
9
Wino.Core.Domain/Interfaces/ICreateAccountAliasDialog.cs
Normal file
9
Wino.Core.Domain/Interfaces/ICreateAccountAliasDialog.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Wino.Core.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Interfaces
|
||||||
|
{
|
||||||
|
public interface ICreateAccountAliasDialog
|
||||||
|
{
|
||||||
|
public MailAccountAlias CreatedAccountAlias { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = "");
|
||||||
}
|
}
|
||||||
|
|||||||
9
Wino.Core.Domain/Models/Accounts/ProfileInformation.cs
Normal file
9
Wino.Core.Domain/Models/Accounts/ProfileInformation.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
76
Wino.Core.Domain/Models/Launch/MailToUri.cs
Normal file
76
Wino.Core.Domain/Models/Launch/MailToUri.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
55
Wino.Core.Domain/Models/MailItem/DraftPreparationRequest.cs
Normal file
55
Wino.Core.Domain/Models/MailItem/DraftPreparationRequest.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
namespace Wino.Core.Domain.Models.Personalization
|
|
||||||
{
|
|
||||||
public record MailListPaneLengthPreferences(string Title, double Length);
|
|
||||||
}
|
|
||||||
12
Wino.Core.Domain/Models/Reader/ImageInfo.cs
Normal file
12
Wino.Core.Domain/Models/Reader/ImageInfo.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
180
Wino.Core.Domain/Translator.Designer.cs
generated
180
Wino.Core.Domain/Translator.Designer.cs
generated
@@ -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.
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
Wino.Core/Services/LaunchProtocolService.cs
Normal file
10
Wino.Core/Services/LaunchProtocolService.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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} <<a href=\"mailto:{x.Address ?? Translator.UnknownAddress}\">{x.Address ?? Translator.UnknownAddress}</a>>"));
|
.Select(x => $"{x.Name ?? Translator.UnknownSender} <<a href=\"mailto:{x.Address ?? Translator.UnknownAddress}\">{x.Address ?? Translator.UnknownAddress}</a>>"));
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
143
Wino.Mail.ViewModels/AliasManagementPageViewModel.cs
Normal file
143
Wino.Mail.ViewModels/AliasManagementPageViewModel.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
27
Wino.Mail/Converters/GridLengthConverter.cs
Normal file
27
Wino.Mail/Converters/GridLengthConverter.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}" />
|
||||||
|
|||||||
40
Wino.Mail/Dialogs/CreateAccountAliasDialog.xaml
Normal file
40
Wino.Mail/Dialogs/CreateAccountAliasDialog.xaml
Normal 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>
|
||||||
|
|
||||||
30
Wino.Mail/Dialogs/CreateAccountAliasDialog.xaml.cs
Normal file
30
Wino.Mail/Dialogs/CreateAccountAliasDialog.xaml.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}">`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user