48 Commits

Author SHA1 Message Date
Burak Kaan Köse
d623129d56 Merge pull request #325 from bkaankose/features/mail-list-splitter
Mail List splitter
2024-08-19 21:21:28 +02:00
Aleh Khantsevich
9cc4c33bb1 Fix multiselect and hide sizer 2024-08-19 21:16:58 +02:00
Burak Kaan Köse
c087b40d4a Renaming event handlers and fixing the search box margin issue. 2024-08-19 21:07:21 +02:00
Aleh Khantsevich
a82e074bd4 Fix merge conflicts 2024-08-19 20:50:14 +02:00
Burak Kaan Köse
3365c099bb Missing comments. 2024-08-19 20:43:26 +02:00
Aleh Khantsevich
d8705de26f Replaced setting with reset button 2024-08-19 20:41:55 +02:00
Burak Kaan Köse
3af181e736 Merge pull request #324 from bkaankose/feature/Aliases
E-mail Aliases Support
2024-08-19 20:37:58 +02:00
Burak Kaan Köse
ba6c01b7c6 Missing comments. 2024-08-19 19:16:54 +02:00
Burak Kaan Köse
7a7cdcb041 Remove json.net dependency to STJson 2024-08-19 19:16:47 +02:00
Burak Kaan Köse
09e52bf199 Remove not implemented comment. 2024-08-19 19:05:54 +02:00
Burak Kaan Köse
a8c39a1587 Fixing send draft issue with OutlookSynchronizer. 2024-08-19 19:02:33 +02:00
Aleh Khantsevich
68536d6c34 Fix padding in narrow state 2024-08-19 18:49:35 +02:00
Aleh Khantsevich
f57c27e755 Fix multiple items selected 2024-08-19 17:15:59 +02:00
Aleh Khantsevich
9a97a27c8a Init 2024-08-19 16:26:15 +02:00
Burak Kaan Köse
07bb90dda9 Refactoring outlook draft creation and sending. 2024-08-19 03:44:16 +02:00
Burak Kaan Köse
3bb156f4da Handling of OutlookSynchronizer alias. 2024-08-18 22:45:23 +02:00
Burak Kaan Köse
e13e0efcc6 Bump some nugets. 2024-08-18 22:44:55 +02:00
Burak Kaan Köse
3ae0a94159 Remove folder init progress bar in mail list page. 2024-08-18 22:27:31 +02:00
Burak Kaan Köse
eec67ec7dc Fixed an issue where re-loading messages with attachments break the included attachment encodings. 2024-08-18 22:25:29 +02:00
Burak Kaan Köse
cf51853eec Removed non-mandatory reply-to validation. 2024-08-18 01:11:23 +02:00
Burak Kaan Köse
67838b28a4 Syntactic sugar. 2024-08-18 01:06:21 +02:00
Burak Kaan Köse
bf68e3b7d5 Fix sending draft issue. 2024-08-18 01:05:43 +02:00
Burak Kaan Köse
91ed0bb8bd Ability to select alias in composer page. 2024-08-17 22:55:58 +02:00
Burak Kaan Köse
55fe791c2a Handling of missing mime downloads and mail processed messages. 2024-08-17 20:19:01 +02:00
Burak Kaan Köse
747efac2ec Reworked aliases. 2024-08-17 19:54:52 +02:00
Burak Kaan Köse
a87df2e9f6 Fixed an issue where deleting account navigates back to mail list of the next account. 2024-08-17 19:54:44 +02:00
Burak Kaan Köse
2e4a664744 More detailed ImapImplementation for ID extension. 2024-08-17 19:53:50 +02:00
Tiktack
579a22ea45 Remove self from reply all when no other recepients (#319) 2024-08-17 15:00:25 +02:00
Burak Kaan Köse
abff850427 Managing account aliases and profile synchronization for outlook and gmail. 2024-08-17 03:43:37 +02:00
Burak Kaan Köse
f1154058ba Fix ascending download for messages. QQ server issue will be handled later. 2024-08-17 00:03:45 +02:00
Burak Kaan Köse
cf9f308b7f Updating aliases during profile sync for Gmail. 2024-08-16 01:29:31 +02:00
Burak Kaan Köse
1791df236c Remove unused extension class. 2024-08-16 01:03:00 +02:00
Burak Kaan Köse
7211f94f08 Try - catch for outlook profile sync. 2024-08-16 00:40:10 +02:00
Burak Kaan Köse
7b0343c87f Added sender name comment for gmail. 2024-08-16 00:37:50 +02:00
Burak Kaan Köse
b80f0276b4 Sender Name and Profile Picture synchronization for Outlook 2024-08-16 00:37:38 +02:00
Burak Kaan Köse
8f66fcbb00 Activated contact service for Gmail to retrieve profile picture and sender name. 2024-08-15 23:57:45 +02:00
Burak Kaan Köse
fe449ee1f3 Comments for alias entity. 2024-08-15 16:13:18 +02:00
Burak Kaan Köse
34d6d95186 Including ReplyToAddress for alias. 2024-08-15 16:11:12 +02:00
Burak Kaan Köse
05ddc0660a Creating MailAccountAlias entity. 2024-08-15 16:02:02 +02:00
Burak Kaan Köse
c6047a8428 Version bump 2024-08-13 23:40:09 +02:00
Burak Kaan Köse
bc4838578e Handling null client connection while sending server response. 2024-08-13 22:57:36 +02:00
Burak Kaan Köse
548996405a Fix incorrect accounts' mails are going to different accounts. 2024-08-13 22:54:36 +02:00
Burak Kaan Köse
a9a5f0bd14 Ascending downloading of mails since some servers require it. 2024-08-13 22:54:14 +02:00
Burak Kaan Köse
ec05ff6123 Optional splash screen. 2024-08-13 19:26:24 +02:00
Burak Kaan Köse
10c7ab421b Setting exception on connection failure. 2024-08-13 16:14:25 +02:00
Burak Kaan Köse
a8a5cc53ea Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-08-13 16:13:25 +02:00
Burak Kaan Köse
8fe48ca438 Fixed an issue where reconnecting doesn't await the handle in the second attempt. 2024-08-13 16:12:34 +02:00
Tiktack
cbd5a515a9 Fix account signature preferences during draft creation (#314)
* Pass account ID instead of account to draft creation method, since account object can be stale.

* Configure await
2024-08-12 00:56:26 +02:00
86 changed files with 2242 additions and 884 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -103,9 +103,9 @@ namespace Wino.Core.Domain.Interfaces
/// Creates a draft MailCopy and MimeMessage based on the given options. /// Creates a draft MailCopy and MimeMessage based on the given options.
/// For forward/reply it would include the referenced message. /// For forward/reply it would include the referenced message.
/// </summary> /// </summary>
/// <param name="composerAccount">Account which should have new draft.</param> /// <param name="accountId">AccountId which should have new draft.</param>
/// <param name="draftCreationOptions">Options like new email/forward/draft.</param> /// <param name="draftCreationOptions">Options like new email/forward/draft.</param>
/// <returns>Draft MailCopy and Draft MimeMessage as base64.</returns> /// <returns>Draft MailCopy and Draft MimeMessage as base64.</returns>
Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(MailAccount composerAccount, DraftCreationOptions draftCreationOptions); Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,13 +2,18 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using MimeKit; using MimeKit;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Extensions;
namespace Wino.Core.Domain.Models.MailItem; namespace Wino.Core.Domain.Models.MailItem;
public class DraftPreparationRequest public class DraftPreparationRequest
{ {
public DraftPreparationRequest(MailAccount account, MailCopy createdLocalDraftCopy, string base64EncodedMimeMessage, MailCopy referenceMailCopy = null) public DraftPreparationRequest(MailAccount account,
MailCopy createdLocalDraftCopy,
string base64EncodedMimeMessage,
DraftCreationReason reason,
MailCopy referenceMailCopy = null)
{ {
Account = account ?? throw new ArgumentNullException(nameof(account)); Account = account ?? throw new ArgumentNullException(nameof(account));
@@ -19,6 +24,7 @@ public class DraftPreparationRequest
// This is additional work when deserialization needed, but not much to do atm. // This is additional work when deserialization needed, but not much to do atm.
Base64LocalDraftMimeMessage = base64EncodedMimeMessage; Base64LocalDraftMimeMessage = base64EncodedMimeMessage;
Reason = reason;
} }
[JsonConstructor] [JsonConstructor]
@@ -29,6 +35,7 @@ public class DraftPreparationRequest
public MailCopy ReferenceMailCopy { get; set; } public MailCopy ReferenceMailCopy { get; set; }
public string Base64LocalDraftMimeMessage { get; set; } public string Base64LocalDraftMimeMessage { get; set; }
public DraftCreationReason Reason { get; set; }
[JsonIgnore] [JsonIgnore]
private MimeMessage createdLocalDraftMimeMessage; private MimeMessage createdLocalDraftMimeMessage;
@@ -44,5 +51,5 @@ public class DraftPreparationRequest
} }
} }
public MailAccount Account { get; } public MailAccount Account { get; set; }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -109,12 +109,12 @@ namespace Wino.Core.UWP.Services
{ {
try try
{ {
ConnectingHandle ??= 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.
@@ -125,10 +125,21 @@ namespace Wino.Core.UWP.Services
Log.Information("Server connection established successfully."); Log.Information("Server connection established successfully.");
} }
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) catch (Exception ex)
{ {
Log.Error(ex, "Failed to connect to the server."); Log.Error(ex, "Failed to connect to the server.");
ConnectingHandle?.TrySetException(ex);
Status = WinoServerConnectionStatus.Failed; Status = WinoServerConnectionStatus.Failed;
return false; return false;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,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;
@@ -51,8 +52,9 @@ namespace Wino.Core.Services
_preferencesService = preferencesService; _preferencesService = preferencesService;
} }
public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(MailAccount composerAccount, DraftCreationOptions draftCreationOptions) public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions)
{ {
var composerAccount = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
var createdDraftMimeMessage = await CreateDraftMimeAsync(composerAccount, draftCreationOptions); var createdDraftMimeMessage = await CreateDraftMimeAsync(composerAccount, draftCreationOptions);
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(composerAccount.Id, SpecialFolderType.Draft); var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(composerAccount.Id, SpecialFolderType.Draft);
@@ -61,12 +63,14 @@ 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 = composerAccount.SenderName, FromName = composerAccount.SenderName,
HasAttachments = false, HasAttachments = false,
Importance = MailImportance.Normal, Importance = MailImportance.Normal,
@@ -620,12 +624,17 @@ namespace Wino.Core.Services
// 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 message = new MimeMessage() var message = new MimeMessage()
{ {
Headers = { { Constants.WinoLocalDraftHeader, Guid.NewGuid().ToString() } }, Headers = { { Constants.WinoLocalDraftHeader, Guid.NewGuid().ToString() } },
From = { new MailboxAddress(account.SenderName, account.Address) }
}; };
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 signature = await GetSignature(account, draftCreationOptions.Reason); var signature = await GetSignature(account, draftCreationOptions.Reason);
@@ -728,6 +737,14 @@ namespace Wino.Core.Services
message.Cc.AddRange(referenceMessage.Cc.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" // Manage "ThreadId-ConversationId"
if (!string.IsNullOrEmpty(referenceMessage.MessageId)) if (!string.IsNullOrEmpty(referenceMessage.MessageId))
{ {

View File

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

View File

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

View File

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

View File

@@ -405,7 +405,7 @@ namespace Wino.Core.Synchronizers
]; ];
} }
public override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) protected override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{ {
var downloadedMessageIds = new List<string>(); var downloadedMessageIds = new List<string>();
@@ -922,7 +922,7 @@ namespace Wino.Core.Synchronizers
} }
// In case of the high input, we'll batch them by 50 to reflect changes quickly. // In case of the high input, we'll batch them by 50 to reflect changes quickly.
var batchedMissingMailIds = missingMailIds.Batch(50).Select(a => new UniqueIdSet(a, SortOrder.Descending)); var batchedMissingMailIds = missingMailIds.Batch(50).Select(a => new UniqueIdSet(a, SortOrder.Ascending));
foreach (var batchMissingMailIds in batchedMissingMailIds) foreach (var batchMissingMailIds in batchedMissingMailIds)
{ {

View File

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

View File

@@ -17,7 +17,8 @@
<PackageReference Include="CommunityToolkit.Diagnostics" Version="8.2.2" /> <PackageReference Include="CommunityToolkit.Diagnostics" Version="8.2.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="Google.Apis.Gmail.v1" Version="1.68.0.3427" /> <PackageReference Include="Google.Apis.Gmail.v1" Version="1.68.0.3427" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.59" /> <PackageReference Include="Google.Apis.PeopleService.v1" Version="1.68.0.3359" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.63" />
<PackageReference Include="HtmlKit" Version="1.1.0" /> <PackageReference Include="HtmlKit" Version="1.1.0" />
<PackageReference Include="IsExternalInit" Version="1.0.3"> <PackageReference Include="IsExternalInit" Version="1.0.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@@ -26,9 +27,9 @@
<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="Nito.AsyncEx.Tasks" Version="5.1.2" /> <PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />

View File

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

View File

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

View File

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

View File

@@ -729,17 +729,26 @@ namespace Wino.Mail.ViewModels
operationAccount = accounts.FirstOrDefault(); operationAccount = accounts.FirstOrDefault();
else else
{ {
// There are multiple accounts and there is no selection.
// Don't list all accounts, but only accounts that belong to Merged Inbox.
if (latestSelectedAccountMenuItem is MergedAccountMenuItem selectedMergedAccountMenuItem) if (latestSelectedAccountMenuItem is MergedAccountMenuItem selectedMergedAccountMenuItem)
{ {
// There are multiple accounts and there is no selection.
// Don't list all accounts, but only accounts that belong to Merged Inbox.
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));
}
} }
} }
@@ -777,9 +786,9 @@ namespace Wino.Mail.ViewModels
MailToUri = _launchProtocolService.MailToUri MailToUri = _launchProtocolService.MailToUri
}; };
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(account, draftOptions).ConfigureAwait(false); var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(account.Id, draftOptions).ConfigureAwait(false);
var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage); var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason);
await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest); await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest);
} }
@@ -795,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)
{ {
@@ -877,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, message.AutomaticallyNavigateFirstItem);
{
await ChangeLoadedAccountAsync(firstAccount);
}
} }
} }

View File

@@ -8,7 +8,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using MimeKit; using MimeKit;
using MimeKit.Utils;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
@@ -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
{ {
@@ -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;
@@ -112,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,
@@ -124,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]
@@ -163,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);
@@ -177,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;
@@ -194,6 +224,8 @@ namespace Wino.Mail.ViewModels
SaveImportance(); SaveImportance();
SaveSubject(); SaveSubject();
SaveFromAddress();
SaveReplyToAddress();
await SaveAttachmentsAsync(); await SaveAttachmentsAsync();
await SaveBodyAsync(); await SaveBodyAsync();
@@ -207,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);
@@ -224,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()
{ {
@@ -234,6 +271,31 @@ 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)
@@ -241,8 +303,7 @@ namespace Wino.Mail.ViewModels
bodyBuilder.SetHtmlBody(await GetHTMLBodyFunction()); bodyBuilder.SetHtmlBody(await GetHTMLBodyFunction());
} }
if (bodyBuilder.HtmlBody != null && bodyBuilder.TextBody != null) CurrentMimeMessage.Body = bodyBuilder.ToMessageBody();
CurrentMimeMessage.Body = bodyBuilder.ToMessageBody();
} }
[RelayCommand(CanExecute = nameof(canSendMail))] [RelayCommand(CanExecute = nameof(canSendMail))]
@@ -280,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)
@@ -288,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.MailToUri != 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.MailToUri = 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;
@@ -386,18 +415,24 @@ 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 else
// DialogService.ShowMIMENotFoundMessage(); 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)
{ {
@@ -416,6 +451,8 @@ namespace Wino.Mail.ViewModels
{ {
// Extract information // Extract information
CurrentMimeMessage = replyingMime;
ToItems.Clear(); ToItems.Clear();
CCItems.Clear(); CCItems.Clear();
BCCItems.Clear(); BCCItems.Clear();
@@ -424,22 +461,22 @@ namespace Wino.Mail.ViewModels
LoadAddressInfo(replyingMime.Cc, CCItems); LoadAddressInfo(replyingMime.Cc, CCItems);
LoadAddressInfo(replyingMime.Bcc, BCCItems); LoadAddressInfo(replyingMime.Bcc, BCCItems);
LoadAttachments(replyingMime.Attachments); LoadAttachments();
if (replyingMime.Cc.Any() || replyingMime.Bcc.Any()) if (replyingMime.Cc.Any() || replyingMime.Bcc.Any())
IsCCBCCVisible = true; 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)
{ {
@@ -459,6 +496,28 @@ namespace Wino.Mail.ViewModels
} }
} }
private void SaveFromAddress()
{
if (SelectedAlias == null) return;
CurrentMimeMessage.From.Clear();
CurrentMimeMessage.From.Add(new MailboxAddress(ComposingAccount.SenderName, SelectedAlias.AliasAddress));
}
private void SaveReplyToAddress()
{
if (SelectedAlias == null) return;
if (!string.IsNullOrEmpty(SelectedAlias.ReplyToAddress))
{
if (!CurrentMimeMessage.ReplyTo.Any(a => a is MailboxAddress mailboxAddress && mailboxAddress.Address == SelectedAlias.ReplyToAddress))
{
CurrentMimeMessage.ReplyTo.Clear();
CurrentMimeMessage.ReplyTo.Add(new MailboxAddress(SelectedAlias.ReplyToAddress, SelectedAlias.ReplyToAddress));
}
}
}
private void SaveAddressInfo(IEnumerable<AddressInformation> addresses, InternetAddressList list) private void SaveAddressInfo(IEnumerable<AddressInformation> addresses, InternetAddressList list)
{ {
list.Clear(); list.Clear();

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,7 +68,7 @@ namespace Wino.Mail.ViewModels
private CancellationTokenSource listManipulationCancellationTokenSource = new CancellationTokenSource(); private CancellationTokenSource listManipulationCancellationTokenSource = new CancellationTokenSource();
public IWinoNavigationService NavigationService { get; } public IWinoNavigationService NavigationService { get; }
public IStatePersistanceService StatePersistanceService { get; } public IStatePersistanceService StatePersistenceService { get; }
public IPreferencesService PreferencesService { get; } public IPreferencesService PreferencesService { get; }
private readonly IMailService _mailService; private readonly IMailService _mailService;
@@ -120,6 +120,12 @@ namespace Wino.Mail.ViewModels
[ObservableProperty] [ObservableProperty]
private string barMessage; private string barMessage;
[ObservableProperty]
private double mailListLength = 420;
[ObservableProperty]
private double maxMailListLength = 1200;
[ObservableProperty] [ObservableProperty]
private string barTitle; private string barTitle;
@@ -141,7 +147,7 @@ namespace Wino.Mail.ViewModels
public MailListPageViewModel(IDialogService dialogService, public MailListPageViewModel(IDialogService dialogService,
IWinoNavigationService navigationService, IWinoNavigationService navigationService,
IMailService mailService, IMailService mailService,
IStatePersistanceService statePersistanceService, IStatePersistanceService statePersistenceService,
IFolderService folderService, IFolderService folderService,
IThreadingStrategyProvider threadingStrategyProvider, IThreadingStrategyProvider threadingStrategyProvider,
IContextMenuItemService contextMenuItemService, IContextMenuItemService contextMenuItemService,
@@ -152,7 +158,7 @@ namespace Wino.Mail.ViewModels
{ {
PreferencesService = preferencesService; PreferencesService = preferencesService;
_winoServerConnectionManager = winoServerConnectionManager; _winoServerConnectionManager = winoServerConnectionManager;
StatePersistanceService = statePersistanceService; StatePersistenceService = statePersistenceService;
NavigationService = navigationService; NavigationService = navigationService;
_mailService = mailService; _mailService = mailService;
@@ -165,6 +171,8 @@ namespace Wino.Mail.ViewModels
SelectedFilterOption = FilterOptions[0]; SelectedFilterOption = FilterOptions[0];
SelectedSortingOption = SortingOptions[0]; SelectedSortingOption = SortingOptions[0];
mailListLength = statePersistenceService.MailListPaneLength;
selectionChangedObservable = Observable.FromEventPattern<NotifyCollectionChangedEventArgs>(SelectedItems, nameof(SelectedItems.CollectionChanged)); selectionChangedObservable = Observable.FromEventPattern<NotifyCollectionChangedEventArgs>(SelectedItems, nameof(SelectedItems.CollectionChanged));
selectionChangedObservable selectionChangedObservable
.Throttle(TimeSpan.FromMilliseconds(100)) .Throttle(TimeSpan.FromMilliseconds(100))
@@ -257,7 +265,7 @@ namespace Wino.Mail.ViewModels
{ {
if (_activeMailItem == selectedMailItemViewModel) return; if (_activeMailItem == selectedMailItemViewModel) return;
// Don't update active mail item if Ctrl key is pressed or multi selection is ennabled. // Don't update active mail item if Ctrl key is pressed or multi selection is enabled.
// User is probably trying to select multiple items. // User is probably trying to select multiple items.
// This is not the same behavior in Windows Mail, // This is not the same behavior in Windows Mail,
// but it's a trash behavior. // but it's a trash behavior.
@@ -266,7 +274,7 @@ namespace Wino.Mail.ViewModels
bool isMultiSelecting = isCtrlKeyPressed || IsMultiSelectionModeEnabled; bool isMultiSelecting = isCtrlKeyPressed || IsMultiSelectionModeEnabled;
if (isMultiSelecting ? StatePersistanceService.IsReaderNarrowed : false) if (isMultiSelecting && StatePersistenceService.IsReaderNarrowed)
{ {
// Don't change the active mail item if the reader is narrowed, but just update the shell. // Don't change the active mail item if the reader is narrowed, but just update the shell.
Messenger.Send(new ShellStateUpdated()); Messenger.Send(new ShellStateUpdated());
@@ -610,6 +618,9 @@ namespace Wino.Mail.ViewModels
if (ActiveFolder == null) return; if (ActiveFolder == null) return;
// At least accounts must match.
if (ActiveFolder.HandlingFolders.Any(a => a.MailAccountId != addedMail.AssignedAccount.Id)) return;
// Messages coming to sent or draft folder must be inserted regardless of the filter. // Messages coming to sent or draft folder must be inserted regardless of the filter.
bool shouldPreventIgnoringFilter = addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft || bool shouldPreventIgnoringFilter = addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft ||
addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent; addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent;
@@ -617,6 +628,7 @@ namespace Wino.Mail.ViewModels
// Item does not belong to this folder and doesn't have special type to be inserted. // Item does not belong to this folder and doesn't have special type to be inserted.
if (!shouldPreventIgnoringFilter && !ActiveFolder.HandlingFolders.Any(a => a.Id == addedMail.AssignedFolder.Id)) return; if (!shouldPreventIgnoringFilter && !ActiveFolder.HandlingFolders.Any(a => a.Id == addedMail.AssignedFolder.Id)) return;
// Item should be prevented from being added to the list due to filter.
if (!shouldPreventIgnoringFilter && ShouldPreventItemAdd(addedMail)) return; if (!shouldPreventIgnoringFilter && ShouldPreventItemAdd(addedMail)) return;
await MailCollection.AddAsync(addedMail); await MailCollection.AddAsync(addedMail);

View File

@@ -271,9 +271,9 @@ namespace Wino.Mail.ViewModels
} }
}; };
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount, draftOptions).ConfigureAwait(false); var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, initializedMailItemViewModel.MailCopy); var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy);
await _requestDelegator.ExecuteAsync(draftPreparationRequest); await _requestDelegator.ExecuteAsync(draftPreparationRequest);

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
<Application x:Class="Wino.App" <Application
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" x:Class="Wino.App"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:controls="using:Wino.Controls" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:selectors="using:Wino.Selectors" xmlns:controls="using:Wino.Controls"
xmlns:wino="using:Wino"> xmlns:selectors="using:Wino.Selectors"
xmlns:wino="using:Wino">
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
@@ -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,9 +69,10 @@
</Style> </Style>
<!-- Default Style for ContentDialog --> <!-- Default Style for ContentDialog -->
<Style x:Key="WinoDialogStyle" <Style
BasedOn="{StaticResource DefaultContentDialogStyle}" x:Key="WinoDialogStyle"
TargetType="ContentDialog" /> BasedOn="{StaticResource DefaultContentDialogStyle}"
TargetType="ContentDialog" />
<!-- Settings Menu Item Template --> <!-- Settings Menu Item Template -->
<Style TargetType="controls:SettingsMenuItemControl"> <Style TargetType="controls:SettingsMenuItemControl">
@@ -74,63 +84,71 @@
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="controls:SettingsMenuItemControl"> <ControlTemplate TargetType="controls:SettingsMenuItemControl">
<Grid> <Grid>
<Button Padding="0" <Button
HorizontalAlignment="Stretch" Padding="0"
HorizontalContentAlignment="Stretch" HorizontalAlignment="Stretch"
Command="{TemplateBinding Command}" HorizontalContentAlignment="Stretch"
CommandParameter="{TemplateBinding CommandParameter}" Command="{TemplateBinding Command}"
IsEnabled="{TemplateBinding IsEnabled}" CommandParameter="{TemplateBinding CommandParameter}"
IsHitTestVisible="{TemplateBinding IsClickable}"> IsEnabled="{TemplateBinding IsEnabled}"
<Grid Height="70" IsHitTestVisible="{TemplateBinding IsClickable}">
Padding="0,6,12,6" <Grid
CornerRadius="4"> Height="70"
Padding="0,6,12,6"
CornerRadius="4">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="50" /> <ColumnDefinition Width="50" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ContentControl HorizontalAlignment="Center" <ContentControl
VerticalAlignment="Center" HorizontalAlignment="Center"
Content="{TemplateBinding Icon}" /> VerticalAlignment="Center"
Content="{TemplateBinding Icon}" />
<Grid Grid.Column="1" <Grid
Margin="4,0" Grid.Column="1"
VerticalAlignment="Center" Margin="4,0"
RowSpacing="3"> VerticalAlignment="Center"
RowSpacing="3">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock VerticalAlignment="Center" <TextBlock
FontWeight="SemiBold" VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}" FontWeight="SemiBold"
Text="{TemplateBinding Title}" /> Style="{StaticResource BodyTextBlockStyle}"
<TextBlock Grid.Row="1" Text="{TemplateBinding Title}" />
VerticalAlignment="Center" <TextBlock
Style="{StaticResource CaptionTextBlockStyle}" Grid.Row="1"
Text="{TemplateBinding Description}" /> VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{TemplateBinding Description}" />
</Grid> </Grid>
<Viewbox Grid.Column="0" <Viewbox
Grid.ColumnSpan="2" Grid.Column="0"
Width="16" Grid.ColumnSpan="2"
Height="16" Width="16"
HorizontalAlignment="Right" Height="16"
VerticalAlignment="Center" HorizontalAlignment="Right"
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsNavigateIconVisible}"> VerticalAlignment="Center"
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsNavigateIconVisible}">
<PathIcon Data="F1 M 5.029297 19.091797 L 14.111328 10 L 5.029297 0.908203 L 5.908203 0.029297 L 15.888672 10 L 5.908203 19.970703 Z " /> <PathIcon Data="F1 M 5.029297 19.091797 L 14.111328 10 L 5.029297 0.908203 L 5.908203 0.029297 L 15.888672 10 L 5.908203 19.970703 Z " />
</Viewbox> </Viewbox>
</Grid> </Grid>
</Button> </Button>
<ContentControl Grid.RowSpan="2" <ContentControl
Margin="0,0,16,0" Grid.RowSpan="2"
HorizontalAlignment="Right" Margin="0,0,16,0"
VerticalAlignment="Center" HorizontalAlignment="Right"
Content="{TemplateBinding SideContent}" VerticalAlignment="Center"
IsHitTestVisible="True" Content="{TemplateBinding SideContent}"
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsNavigateIconVisible, Converter={StaticResource ReverseBooleanToVisibilityConverter}}" /> IsHitTestVisible="True"
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsNavigateIconVisible, Converter={StaticResource ReverseBooleanToVisibilityConverter}}" />
</Grid> </Grid>
</ControlTemplate> </ControlTemplate>
</Setter.Value> </Setter.Value>
@@ -188,16 +206,17 @@
<Image Source="/Assets/FileTypes/type_other.png" /> <Image Source="/Assets/FileTypes/type_other.png" />
</DataTemplate> </DataTemplate>
<selectors:FileAttachmentTypeSelector x:Key="FileTypeIconSelector" <selectors:FileAttachmentTypeSelector
Archive="{StaticResource ArchiveTemplate}" x:Key="FileTypeIconSelector"
Executable="{StaticResource ExecutableTemplate}" Archive="{StaticResource ArchiveTemplate}"
HTML="{StaticResource HTMLTemplate}" Executable="{StaticResource ExecutableTemplate}"
Image="{StaticResource ImageTemplate}" HTML="{StaticResource HTMLTemplate}"
None="{StaticResource NoneTemplate}" Image="{StaticResource ImageTemplate}"
Other="{StaticResource OtherTemplate}" None="{StaticResource NoneTemplate}"
PDF="{StaticResource PDFTemplate}" Other="{StaticResource OtherTemplate}"
RarArchive="{StaticResource RarTemplate}" PDF="{StaticResource PDFTemplate}"
Video="{StaticResource VideoTemplate}" /> RarArchive="{StaticResource RarTemplate}"
Video="{StaticResource VideoTemplate}" />
</ResourceDictionary> </ResourceDictionary>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" /> <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />

View File

@@ -179,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

View File

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

View File

@@ -1,6 +1,7 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <ResourceDictionary
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:xaml="using:Windows.UI.Xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:xaml="using:Windows.UI.Xaml">
<x:String x:Key="ThemeName">Mica</x:String> <x:String x:Key="ThemeName">Mica</x:String>
<x:Boolean x:Key="UseMica">True</x:Boolean> <x:Boolean x:Key="UseMica">True</x:Boolean>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,7 +50,7 @@
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>

View File

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

View File

@@ -11,6 +11,7 @@ using Wino.Core.Domain.Models.Navigation;
using Wino.Helpers; using Wino.Helpers;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages; using Wino.Mail.ViewModels.Messages;
using Wino.Messaging.Client.Mails;
using Wino.Views; using Wino.Views;
using Wino.Views.Account; using Wino.Views.Account;
using Wino.Views.Settings; using Wino.Views.Settings;
@@ -88,6 +89,8 @@ namespace Wino.Services
return typeof(SettingOptionsPage); return typeof(SettingOptionsPage);
case WinoPage.AppPreferencesPage: case WinoPage.AppPreferencesPage:
return typeof(AppPreferencesPage); return typeof(AppPreferencesPage);
case WinoPage.AliasManagementPage:
return typeof(AliasManagementPage);
default: default:
return null; return null;
} }
@@ -114,6 +117,7 @@ namespace Wino.Services
{ {
// No need for new navigation, just refresh the folder. // No need for new navigation, just refresh the folder.
WeakReferenceMessenger.Default.Send(new ActiveMailFolderChangedEvent(folderNavigationArgs.BaseFolderMenuItem, folderNavigationArgs.FolderInitLoadAwaitTask)); WeakReferenceMessenger.Default.Send(new ActiveMailFolderChangedEvent(folderNavigationArgs.BaseFolderMenuItem, folderNavigationArgs.FolderInitLoadAwaitTask));
WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested());
return true; return true;
} }
@@ -136,6 +140,13 @@ namespace Wino.Services
{ {
WeakReferenceMessenger.Default.Send(new NewMailItemRenderingRequestedEvent(mailItemViewModel)); WeakReferenceMessenger.Default.Send(new NewMailItemRenderingRequestedEvent(mailItemViewModel));
} }
else if (listingFrame.Content != null
&& listingFrame.Content.GetType() == GetPageType(WinoPage.IdlePage)
&& pageType == typeof(IdlePage))
{
// Idle -> Idle navigation. Ignore.
return true;
}
else else
{ {
listingFrame.Navigate(pageType, parameter, transitionInfo); listingFrame.Navigate(pageType, parameter, transitionInfo);

View File

@@ -9,6 +9,9 @@
<SolidColorBrush x:Key="FolderSyncBrush">#1abc9c</SolidColorBrush> <SolidColorBrush x:Key="FolderSyncBrush">#1abc9c</SolidColorBrush>
<SolidColorBrush x:Key="AliasUnverifiedBrush">#ff7675</SolidColorBrush>
<SolidColorBrush x:Key="AliasVerifiedBrush">#1abc9c</SolidColorBrush>
<ResourceDictionary.ThemeDictionaries> <ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Name="Light"> <ResourceDictionary x:Name="Light">
<SolidColorBrush x:Key="AttachmentBrush">#fdcb6e</SolidColorBrush> <SolidColorBrush x:Key="AttachmentBrush">#fdcb6e</SolidColorBrush>

View File

@@ -4,4 +4,5 @@
xmlns:converters="using:Wino.Converters"> xmlns:converters="using:Wino.Converters">
<converters:ReverseBooleanToVisibilityConverter x:Key="ReverseBooleanToVisibilityConverter" /> <converters:ReverseBooleanToVisibilityConverter x:Key="ReverseBooleanToVisibilityConverter" />
<converters:ReverseBooleanConverter x:Key="ReverseBooleanConverter" /> <converters:ReverseBooleanConverter x:Key="ReverseBooleanConverter" />
<converters:GridLengthConverter x:Key="GridLengthConverter" />
</ResourceDictionary> </ResourceDictionary>

View File

@@ -0,0 +1,6 @@
using Wino.Mail.ViewModels;
namespace Wino.Views.Abstract
{
public abstract class AliasManagementPageAbstract : BasePage<AliasManagementPageViewModel> { }
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -443,17 +443,9 @@
x:Name="AccountsComboBox" x:Name="AccountsComboBox"
Grid.Column="1" Grid.Column="1"
IsEditable="False" IsEditable="False"
IsEnabled="False" ItemsSource="{x:Bind ViewModel.AvailableAliases, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.Accounts}" DisplayMemberPath="AliasAddress"
SelectedItem="{x:Bind ViewModel.ComposingAccount, Mode=TwoWay}"> SelectedItem="{x:Bind ViewModel.SelectedAlias, Mode=TwoWay}" />
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="entities:MailAccount">
<TextBlock>
<Run Text="{x:Bind Name}" /><Run Text=" (" /><Run Text="{x:Bind Address}" /><Run Text=")" />
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<!-- To --> <!-- To -->
<TextBlock <TextBlock

View File

@@ -39,7 +39,8 @@ namespace Wino.Views
public sealed partial class ComposePage : ComposePageAbstract, public sealed partial class ComposePage : ComposePageAbstract,
IRecipient<NavigationPaneModeChanged>, IRecipient<NavigationPaneModeChanged>,
IRecipient<CreateNewComposeMailRequested>, IRecipient<CreateNewComposeMailRequested>,
IRecipient<ApplicationThemeChanged> IRecipient<ApplicationThemeChanged>,
IRecipient<KillChromiumRequested>
{ {
public bool IsComposerDarkMode public bool IsComposerDarkMode
{ {
@@ -237,12 +238,9 @@ namespace Wino.Views
// Convert files to MailAttachmentViewModel. // Convert files to MailAttachmentViewModel.
foreach (var file in files) foreach (var file in files)
{ {
if (!ViewModel.IncludedAttachments.Any(a => a.FileName == file.Path)) var attachmentViewModel = await file.ToAttachmentViewModelAsync();
{
var attachmentViewModel = await file.ToAttachmentViewModelAsync();
ViewModel.IncludedAttachments.Add(attachmentViewModel); await ViewModel.IncludeAttachmentAsync(attachmentViewModel);
}
} }
} }
@@ -417,13 +415,6 @@ namespace Wino.Views
return await ExecuteScriptFunctionAsync("initializeJodit", fonts, composerFont, composerFontSize, readerFont, readerFontSize); return await ExecuteScriptFunctionAsync("initializeJodit", fonts, composerFont, composerFontSize, readerFont, readerFontSize);
} }
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
DisposeDisposables();
DisposeWebView2();
}
private void DisposeWebView2() private void DisposeWebView2()
{ {
@@ -700,5 +691,11 @@ namespace Wino.Views
ToBox.Focus(FocusState.Programmatic); ToBox.Focus(FocusState.Programmatic);
} }
} }
public void Receive(KillChromiumRequested message)
{
DisposeDisposables();
DisposeWebView2();
}
} }
} }

View File

@@ -6,6 +6,7 @@
xmlns:collections="using:CommunityToolkit.Mvvm.Collections" xmlns:collections="using:CommunityToolkit.Mvvm.Collections"
xmlns:controls="using:Wino.Controls" xmlns:controls="using:Wino.Controls"
xmlns:controls1="using:CommunityToolkit.WinUI.Controls" xmlns:controls1="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:Wino.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:domain="using:Wino.Core.Domain" xmlns:domain="using:Wino.Core.Domain"
xmlns:enums="using:Wino.Core.Domain.Enums" xmlns:enums="using:Wino.Core.Domain.Enums"
@@ -20,9 +21,8 @@
xmlns:ui="using:Microsoft.Toolkit.Uwp.UI" xmlns:ui="using:Microsoft.Toolkit.Uwp.UI"
xmlns:viewModelData="using:Wino.Mail.ViewModels.Data" xmlns:viewModelData="using:Wino.Mail.ViewModels.Data"
xmlns:wino="using:Wino" xmlns:wino="using:Wino"
xmlns:converters="using:Wino.Converters"
x:Name="root" x:Name="root"
Loaded="MailListPageLoaded" SizeChanged="PageSizeChanged"
mc:Ignorable="d"> mc:Ignorable="d">
<Page.Resources> <Page.Resources>
@@ -181,11 +181,11 @@
<DataTemplate x:DataType="viewModelData:ThreadMailItemViewModel"> <DataTemplate x:DataType="viewModelData:ThreadMailItemViewModel">
<controls:WinoExpander <controls:WinoExpander
x:Name="ThreadExpander" x:Name="ThreadExpander"
Padding="0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
BackgroundSizing="InnerBorderEdge" BackgroundSizing="InnerBorderEdge"
BorderThickness="0" BorderThickness="0"
Padding="0"
IsExpanded="{x:Bind IsThreadExpanded, Mode=TwoWay}"> IsExpanded="{x:Bind IsThreadExpanded, Mode=TwoWay}">
<muxc:Expander.Header> <muxc:Expander.Header>
<controls:MailItemDisplayInformationControl <controls:MailItemDisplayInformationControl
@@ -276,9 +276,9 @@
<AutoSuggestBox <AutoSuggestBox
x:Name="SearchBar" x:Name="SearchBar"
Margin="2,0,-2,0"
VerticalAlignment="Center" VerticalAlignment="Center"
BorderBrush="Transparent" BorderBrush="Transparent"
Margin="2,0,-2,0"
GotFocus="SearchBoxFocused" GotFocus="SearchBoxFocused"
LostFocus="SearchBarUnfocused" LostFocus="SearchBarUnfocused"
PlaceholderText="{x:Bind domain:Translator.SearchBarPlaceholder}" PlaceholderText="{x:Bind domain:Translator.SearchBarPlaceholder}"
@@ -296,23 +296,20 @@
<Grid x:Name="RootGrid" Padding="0,0,0,7"> <Grid x:Name="RootGrid" Padding="0,0,0,7">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition <ColumnDefinition x:Name="MailListColumn" Width="{x:Bind ViewModel.MailListLength, Mode=OneWay, Converter={StaticResource GridLengthConverter}}" />
x:Name="ReaderColumn"
Width="*"
MaxWidth="{x:Bind ViewModel.StatePersistanceService.MailListPaneLength, Mode=OneWay}" />
<ColumnDefinition x:Name="RendererColumn" Width="*" /> <ColumnDefinition x:Name="RendererColumn" Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Mail Items --> <!-- Mail Items -->
<Border <Border
x:Name="ReaderGridContainer" x:Name="MailListContainer"
Grid.Column="0" Grid.Column="0"
Padding="5,0,0,0" Padding="5,0,0,0"
Background="{ThemeResource WinoContentZoneBackgroud}" Background="{ThemeResource WinoContentZoneBackgroud}"
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}" BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
BorderThickness="1" BorderThickness="1"
CornerRadius="7"> CornerRadius="7">
<Grid x:Name="ReaderGrid"> <Grid x:Name="MailListGrid">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -335,11 +332,6 @@
Padding="2,0" Padding="2,0"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
CornerRadius="8"> CornerRadius="8">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="2" />
</Grid.RowDefinitions>
<!-- Commands --> <!-- Commands -->
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -481,12 +473,6 @@
</Button.Flyout> </Button.Flyout>
</Button> </Button>
</Grid> </Grid>
<muxc:ProgressBar
x:Name="LoadingProgressBar"
Grid.Row="1"
x:Load="{x:Bind ViewModel.IsInitializingFolder, Mode=OneWay}"
IsIndeterminate="True" />
</Grid> </Grid>
<!-- Pivot + Sync + Multi Select --> <!-- Pivot + Sync + Multi Select -->
@@ -615,11 +601,11 @@
<SemanticZoom x:Name="SemanticZoomContainer" CanChangeViews="{x:Bind ViewModel.PreferencesService.IsSemanticZoomEnabled, Mode=OneWay}"> <SemanticZoom x:Name="SemanticZoomContainer" CanChangeViews="{x:Bind ViewModel.PreferencesService.IsSemanticZoomEnabled, Mode=OneWay}">
<SemanticZoom.ZoomedInView> <SemanticZoom.ZoomedInView>
<listview:WinoListView <listview:WinoListView
ui:ScrollViewerExtensions.VerticalScrollBarMargin="0"
x:Name="MailListView" x:Name="MailListView"
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
ui:ScrollViewerExtensions.EnableMiddleClickScrolling="True"
ui:ListViewExtensions.ItemContainerStretchDirection="Horizontal" ui:ListViewExtensions.ItemContainerStretchDirection="Horizontal"
ui:ScrollViewerExtensions.EnableMiddleClickScrolling="True"
ui:ScrollViewerExtensions.VerticalScrollBarMargin="0"
ItemDeletedCommand="{x:Bind ViewModel.MailOperationCommand}" ItemDeletedCommand="{x:Bind ViewModel.MailOperationCommand}"
ItemTemplateSelector="{StaticResource MailItemDisplaySelector}" ItemTemplateSelector="{StaticResource MailItemDisplaySelector}"
ItemsSource="{x:Bind MailCollectionViewSource.View, Mode=OneWay}" ItemsSource="{x:Bind MailCollectionViewSource.View, Mode=OneWay}"
@@ -695,74 +681,39 @@
</Grid> </Grid>
</Border> </Border>
<controls1:PropertySizer
x:Name="MailListSizer"
Grid.Column="1"
Width="16"
HorizontalAlignment="Left"
Binding="{x:Bind ViewModel.MailListLength, Mode=TwoWay}"
Canvas.ZIndex="20"
ManipulationCompleted="MailListSizerManipulationCompleted"
Maximum="{x:Bind ViewModel.MaxMailListLength, Mode=OneWay}"
Minimum="270"
Opacity="0" />
<Grid Grid.Column="1" x:Name="RenderingGrid"> <Grid x:Name="RenderingGrid" Grid.Column="1">
<!-- Mail Rendering Frame --> <!-- Mail Rendering Frame -->
<Frame x:Name="RenderingFrame" IsNavigationStackEnabled="False" /> <Frame x:Name="RenderingFrame" IsNavigationStackEnabled="False" />
<!-- No Mail Selected Message --> <!-- No Mail Selected Message -->
<StackPanel <StackPanel
x:Name="NoMailSelectedPanel" x:Name="NoMailSelectedPanel"
Opacity="0.5"
Spacing="6"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center"> VerticalAlignment="Center"
Opacity="0.5"
Spacing="6">
<controls:WinoFontIcon Icon="Mail" FontSize="80" /> <controls:WinoFontIcon FontSize="80" Icon="Mail" />
<TextBlock <TextBlock
x:Name="CountTextBlock"
HorizontalAlignment="Center" HorizontalAlignment="Center"
FontSize="31" FontSize="31"
Text="{x:Bind ViewModel.SelectedMessageText, Mode=OneWay}"
Style="{StaticResource SubheaderTextBlockStyle}" Style="{StaticResource SubheaderTextBlockStyle}"
x:Name="CountTextBlock" /> Text="{x:Bind ViewModel.SelectedMessageText, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="AdaptiveStates" CurrentStateChanged="AdaptivenessChanged">
<VisualState x:Name="NormalState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="{x:Bind helpers:XamlHelpers.MailListAdaptivityConverter(ViewModel.StatePersistanceService.MailListPaneLength), Mode=OneWay}" />
</VisualState.StateTriggers>
</VisualState>
<VisualState x:Name="NarrowState">
<VisualState.Setters>
<Setter Target="ReaderGrid.MaxWidth" Value="10000" />
</VisualState.Setters>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
<!-- Pane open adjustments -->
<VisualStateGroup x:Name="PaneStates">
<VisualState x:Name="PaneOpened">
<VisualState.Setters>
<!--<Setter Target="RootGrid.Margin" Value="-6,0,0,0" />-->
<Setter Target="ReaderGrid.Margin" Value="-4,0,0,0" />
</VisualState.Setters>
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind PreferencesService.IsNavigationPaneOpened, Mode=OneWay}" />
</VisualState.StateTriggers>
</VisualState>
<VisualState x:Name="PaneClosed" />
</VisualStateGroup>
<!-- Mail reader states -->
<VisualStateGroup x:Name="ReaderStates">
<VisualState x:Name="ReaderActive">
<VisualState.Setters>
<Setter Target="ReaderGrid.CornerRadius" Value="0" />
</VisualState.Setters>
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind StatePersistanceService.IsReadingMail, Mode=OneWay}" />
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid> </Grid>
</abstract:MailListPageAbstract> </abstract:MailListPageAbstract>

View File

@@ -40,11 +40,9 @@ namespace Wino.Views
IRecipient<ShellStateUpdated>, IRecipient<ShellStateUpdated>,
IRecipient<DisposeRenderingFrameRequested> IRecipient<DisposeRenderingFrameRequested>
{ {
private const string NarrowVisualStateKey = "NarrowState"; private const double RENDERING_COLUMN_MIN_WIDTH = 300;
private const string AdaptivenessStatesKey = "AdaptiveStates";
private IStatePersistanceService StatePersistanceService { get; } = App.Current.Services.GetService<IStatePersistanceService>(); private IStatePersistanceService StatePersistenceService { get; } = App.Current.Services.GetService<IStatePersistanceService>();
private IPreferencesService PreferencesService { get; } = App.Current.Services.GetService<IPreferencesService>();
private IKeyPressService KeyPressService { get; } = App.Current.Services.GetService<IKeyPressService>(); private IKeyPressService KeyPressService { get; } = App.Current.Services.GetService<IKeyPressService>();
public MailListPage() public MailListPage()
@@ -90,7 +88,7 @@ namespace Wino.Views
SelectAllCheckbox.Unchecked += SelectAllCheckboxUnchecked; SelectAllCheckbox.Unchecked += SelectAllCheckboxUnchecked;
} }
private void SelectionModeToggleChecked(object sender, Windows.UI.Xaml.RoutedEventArgs e) private void SelectionModeToggleChecked(object sender, RoutedEventArgs e)
{ {
ChangeSelectionMode(ListViewSelectionMode.Multiple); ChangeSelectionMode(ListViewSelectionMode.Multiple);
} }
@@ -132,92 +130,21 @@ namespace Wino.Views
} }
} }
private void SelectionModeToggleUnchecked(object sender, Windows.UI.Xaml.RoutedEventArgs e) private void SelectionModeToggleUnchecked(object sender, RoutedEventArgs e)
{ {
ChangeSelectionMode(ListViewSelectionMode.Extended); ChangeSelectionMode(ListViewSelectionMode.Extended);
} }
private void SelectAllCheckboxChecked(object sender, Windows.UI.Xaml.RoutedEventArgs e) private void SelectAllCheckboxChecked(object sender, RoutedEventArgs e)
{ {
MailListView.SelectAllWino(); MailListView.SelectAllWino();
} }
private void SelectAllCheckboxUnchecked(object sender, Windows.UI.Xaml.RoutedEventArgs e) private void SelectAllCheckboxUnchecked(object sender, RoutedEventArgs e)
{ {
MailListView.ClearSelections(); MailListView.ClearSelections();
} }
#region Mostly UI
private void UpdateAdaptiveness()
{
bool shouldDisplayNoMessagePanel, shouldDisplayMailingList, shouldDisplayRenderingFrame;
// This is the smallest state UI can get.
// Either mailing list or rendering grid is visible.
if (StatePersistanceService.IsReaderNarrowed)
{
// Start visibility checks by no message panel.
bool isMultiSelectionEnabled = ViewModel.IsMultiSelectionModeEnabled || KeyPressService.IsCtrlKeyPressed();
shouldDisplayMailingList = isMultiSelectionEnabled ? true : (!ViewModel.HasSelectedItems || ViewModel.HasMultipleItemSelections);
shouldDisplayNoMessagePanel = shouldDisplayMailingList ? false : !ViewModel.HasSelectedItems || ViewModel.HasMultipleItemSelections;
shouldDisplayRenderingFrame = shouldDisplayMailingList ? false : !shouldDisplayNoMessagePanel;
}
else
{
shouldDisplayMailingList = true;
shouldDisplayNoMessagePanel = !ViewModel.HasSelectedItems || ViewModel.HasMultipleItemSelections;
shouldDisplayRenderingFrame = !shouldDisplayNoMessagePanel;
}
ReaderGridContainer.Visibility = shouldDisplayMailingList ? Visibility.Visible : Visibility.Collapsed;
RenderingFrame.Visibility = shouldDisplayRenderingFrame ? Visibility.Visible : Visibility.Collapsed;
NoMailSelectedPanel.Visibility = shouldDisplayNoMessagePanel ? Visibility.Visible : Visibility.Collapsed;
if (StatePersistanceService.IsReaderNarrowed)
{
if (RenderingFrame.Visibility == Visibility.Visible && ReaderGridContainer.Visibility == Visibility.Collapsed)
{
// Extend rendering frame to full width.
Grid.SetColumn(RenderingGrid, 0);
Grid.SetColumnSpan(RenderingGrid, 2);
Grid.SetColumn(ReaderGrid, 0);
Grid.SetColumnSpan(ReaderGrid, 2);
}
else if (RenderingFrame.Visibility == Visibility.Collapsed && NoMailSelectedPanel.Visibility == Visibility.Collapsed)
{
// Only mail list is available.
// Extend the mailing list.
Grid.SetColumn(ReaderGridContainer, 0);
Grid.SetColumnSpan(ReaderGridContainer, 2);
}
}
else
{
// Mailing list is always visible on the first part.
Grid.SetColumn(ReaderGridContainer, 0);
Grid.SetColumnSpan(ReaderGridContainer, 1);
// Rendering grid should take the rest of the space.
Grid.SetColumn(RenderingGrid, 1);
Grid.SetColumnSpan(RenderingGrid, 1);
}
}
private void AdaptivenessChanged(object sender, VisualStateChangedEventArgs e)
{
StatePersistanceService.IsReaderNarrowed = e.NewState.Name == "NarrowState";
UpdateAdaptiveness();
}
#endregion
void IRecipient<ResetSingleMailItemSelectionEvent>.Receive(ResetSingleMailItemSelectionEvent message) void IRecipient<ResetSingleMailItemSelectionEvent>.Receive(ResetSingleMailItemSelectionEvent message)
{ {
// Single item in thread selected. // Single item in thread selected.
@@ -446,21 +373,6 @@ namespace Wino.Views
SearchBar.PlaceholderText = Translator.SearchBarPlaceholder; SearchBar.PlaceholderText = Translator.SearchBarPlaceholder;
} }
private void MailListPageLoaded(object sender, RoutedEventArgs e)
{
// App might open with narrowed state.
// VSM will not trigger in this case.
// Set values to force updating adaptiveness.
var groups = VisualStateManager.GetVisualStateGroups(RootGrid);
var adaptiveState = groups.FirstOrDefault(a => a.Name == AdaptivenessStatesKey);
if (adaptiveState == null) return;
// This should force UpdateAdaptiveness call.
StatePersistanceService.IsReaderNarrowed = adaptiveState.CurrentState.Name == NarrowVisualStateKey;
}
private void ProcessMailItemKeyboardAccelerator(UIElement sender, ProcessKeyboardAcceleratorEventArgs args) private void ProcessMailItemKeyboardAccelerator(UIElement sender, ProcessKeyboardAcceleratorEventArgs args)
{ {
if (args.Key == Windows.System.VirtualKey.Delete) if (args.Key == Windows.System.VirtualKey.Delete)
@@ -568,5 +480,89 @@ namespace Wino.Views
{ {
ViewModel.NavigationService.Navigate(WinoPage.IdlePage, null, NavigationReferenceFrame.RenderingFrame, NavigationTransitionType.DrillIn); ViewModel.NavigationService.Navigate(WinoPage.IdlePage, null, NavigationReferenceFrame.RenderingFrame, NavigationTransitionType.DrillIn);
} }
private void PageSizeChanged(object sender, SizeChangedEventArgs e)
{
ViewModel.MaxMailListLength = e.NewSize.Width - RENDERING_COLUMN_MIN_WIDTH;
StatePersistenceService.IsReaderNarrowed = e.NewSize.Width < StatePersistenceService.MailListPaneLength + RENDERING_COLUMN_MIN_WIDTH;
UpdateAdaptiveness();
}
private void MailListSizerManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
StatePersistenceService.MailListPaneLength = ViewModel.MailListLength;
}
private void UpdateAdaptiveness()
{
bool shouldDisplayNoMessagePanel, shouldDisplayMailingList, shouldDisplayRenderingFrame;
bool isMultiSelectionEnabled = ViewModel.IsMultiSelectionModeEnabled || KeyPressService.IsCtrlKeyPressed();
// This is the smallest state UI can get.
// Either mailing list or rendering grid is visible.
if (StatePersistenceService.IsReaderNarrowed)
{
// Start visibility checks by no message panel.
shouldDisplayMailingList = isMultiSelectionEnabled ? true : (!ViewModel.HasSelectedItems || ViewModel.HasMultipleItemSelections);
shouldDisplayNoMessagePanel = shouldDisplayMailingList ? false : !ViewModel.HasSelectedItems || ViewModel.HasMultipleItemSelections;
shouldDisplayRenderingFrame = shouldDisplayMailingList ? false : !shouldDisplayNoMessagePanel;
}
else
{
shouldDisplayMailingList = true;
shouldDisplayNoMessagePanel = !ViewModel.HasSelectedItems || ViewModel.HasMultipleItemSelections;
shouldDisplayRenderingFrame = !shouldDisplayNoMessagePanel;
}
MailListContainer.Visibility = shouldDisplayMailingList ? Visibility.Visible : Visibility.Collapsed;
RenderingFrame.Visibility = shouldDisplayRenderingFrame ? Visibility.Visible : Visibility.Collapsed;
NoMailSelectedPanel.Visibility = shouldDisplayNoMessagePanel ? Visibility.Visible : Visibility.Collapsed;
if (StatePersistenceService.IsReaderNarrowed == true)
{
if (ViewModel.HasSingleItemSelection && !isMultiSelectionEnabled)
{
MailListColumn.Width = new GridLength(0);
RendererColumn.Width = new GridLength(1, GridUnitType.Star);
Grid.SetColumn(MailListContainer, 0);
Grid.SetColumnSpan(RenderingGrid, 2);
MailListContainer.Visibility = Visibility.Collapsed;
RenderingGrid.Visibility = Visibility.Visible;
}
else
{
MailListColumn.Width = new GridLength(1, GridUnitType.Star);
RendererColumn.Width = new GridLength(0);
Grid.SetColumnSpan(MailListContainer, 2);
MailListContainer.Margin = new Thickness(7, 0, 7, 0);
MailListContainer.Visibility = Visibility.Visible;
RenderingGrid.Visibility = Visibility.Collapsed;
SearchBar.Margin = new Thickness(8, 0, -2, 0);
MailListSizer.Visibility = Visibility.Collapsed;
}
}
else
{
MailListColumn.Width = new GridLength(StatePersistenceService.MailListPaneLength);
RendererColumn.Width = new GridLength(1, GridUnitType.Star);
MailListContainer.Margin = new Thickness(0, 0, 0, 0);
Grid.SetColumn(MailListContainer, 0);
Grid.SetColumn(RenderingGrid, 1);
Grid.SetColumnSpan(MailListContainer, 1);
Grid.SetColumnSpan(RenderingGrid, 1);
MailListContainer.Visibility = Visibility.Visible;
RenderingGrid.Visibility = Visibility.Visible;
SearchBar.Margin = new Thickness(2, 0, -2, 0);
MailListSizer.Visibility = Visibility.Visible;
}
}
} }
} }

View File

@@ -44,6 +44,7 @@ namespace Wino.Views
WinoPage.SignatureManagementPage => typeof(SignatureManagementPage), WinoPage.SignatureManagementPage => typeof(SignatureManagementPage),
WinoPage.AccountDetailsPage => typeof(AccountDetailsPage), WinoPage.AccountDetailsPage => typeof(AccountDetailsPage),
WinoPage.MergedAccountDetailsPage => typeof(MergedAccountDetailsPage), WinoPage.MergedAccountDetailsPage => typeof(MergedAccountDetailsPage),
WinoPage.AliasManagementPage => typeof(AliasManagementPage),
_ => null, _ => null,
}; };
} }

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
using Wino.Views.Abstract;
namespace Wino.Views.Settings
{
public sealed partial class AliasManagementPage : AliasManagementPageAbstract
{
public AliasManagementPage()
{
this.InitializeComponent();
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -169,6 +169,9 @@
<PackageReference Include="CommunityToolkit.Uwp.Animations"> <PackageReference Include="CommunityToolkit.Uwp.Animations">
<Version>8.0.240109</Version> <Version>8.0.240109</Version>
</PackageReference> </PackageReference>
<PackageReference Include="CommunityToolkit.Uwp.Behaviors">
<Version>8.0.240109</Version>
</PackageReference>
<PackageReference Include="CommunityToolkit.Uwp.Controls.SettingsControls"> <PackageReference Include="CommunityToolkit.Uwp.Controls.SettingsControls">
<Version>8.0.240109</Version> <Version>8.0.240109</Version>
</PackageReference> </PackageReference>
@@ -246,6 +249,7 @@
<Compile Include="Controls\WinoFontIconSource.cs" /> <Compile Include="Controls\WinoFontIconSource.cs" />
<Compile Include="Controls\WinoFontIcon.cs" /> <Compile Include="Controls\WinoFontIcon.cs" />
<Compile Include="Controls\WinoSwipeControlItems.cs" /> <Compile Include="Controls\WinoSwipeControlItems.cs" />
<Compile Include="Converters\GridLengthConverter.cs" />
<Compile Include="Dialogs\AccountEditDialog.xaml.cs"> <Compile Include="Dialogs\AccountEditDialog.xaml.cs">
<DependentUpon>AccountEditDialog.xaml</DependentUpon> <DependentUpon>AccountEditDialog.xaml</DependentUpon>
</Compile> </Compile>
@@ -268,6 +272,9 @@
<Compile Include="Dialogs\NewImapSetupDialog.xaml.cs"> <Compile Include="Dialogs\NewImapSetupDialog.xaml.cs">
<DependentUpon>NewImapSetupDialog.xaml</DependentUpon> <DependentUpon>NewImapSetupDialog.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Dialogs\CreateAccountAliasDialog.xaml.cs">
<DependentUpon>CreateAccountAliasDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Dialogs\StoreRatingDialog.xaml.cs"> <Compile Include="Dialogs\StoreRatingDialog.xaml.cs">
<DependentUpon>StoreRatingDialog.xaml</DependentUpon> <DependentUpon>StoreRatingDialog.xaml</DependentUpon>
</Compile> </Compile>
@@ -340,6 +347,7 @@
<Compile Include="Views\Abstract\AboutPageAbstract.cs" /> <Compile Include="Views\Abstract\AboutPageAbstract.cs" />
<Compile Include="Views\Abstract\AccountDetailsPageAbstract.cs" /> <Compile Include="Views\Abstract\AccountDetailsPageAbstract.cs" />
<Compile Include="Views\Abstract\AccountManagementPageAbstract.cs" /> <Compile Include="Views\Abstract\AccountManagementPageAbstract.cs" />
<Compile Include="Views\Abstract\AliasManagementPageAbstract.cs" />
<Compile Include="Views\Abstract\AppPreferencesPageAbstract.cs" /> <Compile Include="Views\Abstract\AppPreferencesPageAbstract.cs" />
<Compile Include="Views\Abstract\AppShellAbstract.cs" /> <Compile Include="Views\Abstract\AppShellAbstract.cs" />
<Compile Include="Views\Abstract\ComposePageAbstract.cs" /> <Compile Include="Views\Abstract\ComposePageAbstract.cs" />
@@ -423,6 +431,9 @@
<Compile Include="Views\Settings\SettingOptionsPage.xaml.cs"> <Compile Include="Views\Settings\SettingOptionsPage.xaml.cs">
<DependentUpon>SettingOptionsPage.xaml</DependentUpon> <DependentUpon>SettingOptionsPage.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Views\Settings\AliasManagementPage.xaml.cs">
<DependentUpon>AliasManagementPage.xaml</DependentUpon>
</Compile>
<Compile Include="Views\Settings\SignatureManagementPage.xaml.cs"> <Compile Include="Views\Settings\SignatureManagementPage.xaml.cs">
<DependentUpon>SignatureManagementPage.xaml</DependentUpon> <DependentUpon>SignatureManagementPage.xaml</DependentUpon>
</Compile> </Compile>
@@ -509,6 +520,10 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Include="Dialogs\CreateAccountAliasDialog.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Dialogs\StoreRatingDialog.xaml"> <Page Include="Dialogs\StoreRatingDialog.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@@ -657,6 +672,10 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Include="Views\Settings\AliasManagementPage.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\Settings\SignatureManagementPage.xaml"> <Page Include="Views\Settings\SignatureManagementPage.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>

View File

@@ -0,0 +1,7 @@
namespace Wino.Messaging.Client.Mails
{
/// <summary>
/// Terminates all chromum instances.
/// </summary>
public record KillChromiumRequested;
}

View File

@@ -11,7 +11,7 @@
<Identity <Identity
Name="58272BurakKSE.WinoMailPreview" Name="58272BurakKSE.WinoMailPreview"
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911" Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
Version="1.8.0.0" /> Version="1.8.2.0" />
<Extensions> <Extensions>
<!-- Publisher Cache Folders --> <!-- Publisher Cache Folders -->
@@ -48,7 +48,7 @@
Square150x150Logo="Images\Square150x150Logo.png" Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png"> Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/> <uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/>
<uap:SplashScreen Image="Images\SplashScreen.png" /> <uap:SplashScreen Image="Images\SplashScreen.png" uap5:Optional="true" />
</uap:VisualElements> </uap:VisualElements>
<Extensions> <Extensions>

View File

@@ -52,7 +52,7 @@ namespace Wino.Server.MessageHandlers
{ {
var synchronizationResult = await synchronizer.SynchronizeAsync(message.Options, cancellationToken).ConfigureAwait(false); var synchronizationResult = await synchronizer.SynchronizeAsync(message.Options, cancellationToken).ConfigureAwait(false);
if (synchronizationResult.DownloadedMessages.Any()) if (synchronizationResult.DownloadedMessages?.Any() ?? false || !synchronizer.Account.Preferences.IsNotificationsEnabled)
{ {
var accountInboxFolder = await _folderService.GetSpecialFolderByAccountIdAsync(message.Options.AccountId, SpecialFolderType.Inbox); var accountInboxFolder = await _folderService.GetSpecialFolderByAccountIdAsync(message.Options.AccountId, SpecialFolderType.Inbox);

View File

@@ -219,7 +219,20 @@ namespace Wino.Server
{ MessageConstants.MessageDataTypeKey, message.GetType().Name } { MessageConstants.MessageDataTypeKey, message.GetType().Name }
}; };
await connection.SendMessageAsync(set); try
{
await connection.SendMessageAsync(set);
}
catch (InvalidOperationException)
{
// Connection might've been disposed during the SendMessageAsync call.
// This is a safe way to handle the exception.
// We don't lock the connection since this request may take sometime to complete.
}
catch (Exception exception)
{
Log.Error(exception, "SendMessageAsync threw an exception");
}
} }
private void OnConnectionClosed(AppServiceConnection sender, AppServiceClosedEventArgs args) private void OnConnectionClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)

View File

@@ -31,8 +31,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="H.NotifyIcon.Wpf" Version="2.1.0" /> <PackageReference Include="H.NotifyIcon.Wpf" Version="2.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.0.3" /> <PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" /> <ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />