Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d623129d56 | ||
|
|
9cc4c33bb1 | ||
|
|
c087b40d4a | ||
|
|
a82e074bd4 | ||
|
|
3365c099bb | ||
|
|
d8705de26f | ||
|
|
3af181e736 | ||
|
|
ba6c01b7c6 | ||
|
|
7a7cdcb041 | ||
|
|
09e52bf199 | ||
|
|
a8c39a1587 | ||
|
|
68536d6c34 | ||
|
|
f57c27e755 | ||
|
|
9a97a27c8a | ||
|
|
07bb90dda9 | ||
|
|
3bb156f4da | ||
|
|
e13e0efcc6 | ||
|
|
3ae0a94159 | ||
|
|
eec67ec7dc | ||
|
|
cf51853eec | ||
|
|
67838b28a4 | ||
|
|
bf68e3b7d5 | ||
|
|
91ed0bb8bd | ||
|
|
55fe791c2a | ||
|
|
747efac2ec | ||
|
|
a87df2e9f6 | ||
|
|
2e4a664744 | ||
|
|
579a22ea45 | ||
|
|
abff850427 | ||
|
|
f1154058ba | ||
|
|
cf9f308b7f | ||
|
|
1791df236c | ||
|
|
7211f94f08 | ||
|
|
7b0343c87f | ||
|
|
b80f0276b4 | ||
|
|
8f66fcbb00 | ||
|
|
fe449ee1f3 | ||
|
|
34d6d95186 | ||
|
|
05ddc0660a | ||
|
|
c6047a8428 |
@@ -44,6 +44,11 @@ namespace Wino.Core.Domain.Entities
|
||||
/// </summary>
|
||||
public string AccountColorHex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Base64 encoded profile picture of the account.
|
||||
/// </summary>
|
||||
public string Base64ProfilePictureData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the listing order of the account in the accounts list.
|
||||
/// </summary>
|
||||
@@ -78,5 +83,15 @@ namespace Wino.Core.Domain.Entities
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public MailAccountPreferences Preferences { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the account can perform ProfileInformation sync type.
|
||||
/// </summary>
|
||||
public bool IsProfileInfoSyncSupported => ProviderType == MailProviderType.Outlook || ProviderType == MailProviderType.Office365 || ProviderType == MailProviderType.Gmail;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the account can perform AliasInformation sync type.
|
||||
/// </summary>
|
||||
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail;
|
||||
}
|
||||
}
|
||||
|
||||
56
Wino.Core.Domain/Entities/MailAccountAlias.cs
Normal file
56
Wino.Core.Domain/Entities/MailAccountAlias.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace Wino.Core.Domain.Entities
|
||||
{
|
||||
public class RemoteAccountAlias
|
||||
{
|
||||
/// <summary>
|
||||
/// Display address of the alias.
|
||||
/// </summary>
|
||||
public string AliasAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Address to be included in Reply-To header when alias is used for sending messages.
|
||||
/// </summary>
|
||||
public string ReplyToAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this alias is the primary alias for the account.
|
||||
/// </summary>
|
||||
public bool IsPrimary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the alias is verified by the server.
|
||||
/// Only Gmail aliases are verified for now.
|
||||
/// Non-verified alias messages might be rejected by SMTP server.
|
||||
/// </summary>
|
||||
public bool IsVerified { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this alias is the root alias for the account.
|
||||
/// Root alias means the first alias that was created for the account.
|
||||
/// It can't be deleted or changed.
|
||||
/// </summary>
|
||||
public bool IsRootAlias { get; set; }
|
||||
}
|
||||
|
||||
public class MailAccountAlias : RemoteAccountAlias
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique Id for the alias.
|
||||
/// </summary>
|
||||
[PrimaryKey]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Account id that this alias is attached to.
|
||||
/// </summary>
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Root aliases can't be deleted.
|
||||
/// </summary>
|
||||
public bool CanDelete => !IsRootAlias;
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,7 @@ namespace Wino.Core.Domain.Entities
|
||||
/// </summary>
|
||||
[Ignore]
|
||||
public MailAccount AssignedAccount { get; set; }
|
||||
public IEnumerable<Guid> GetContainingIds() => new[] { UniqueId };
|
||||
public IEnumerable<Guid> GetContainingIds() => [UniqueId];
|
||||
public override string ToString() => $"{Subject} <-> {Id}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
ManuelSetupWaiting,
|
||||
TestingConnection,
|
||||
AutoDiscoverySetup,
|
||||
AutoDiscoveryInProgress
|
||||
AutoDiscoveryInProgress,
|
||||
FetchingProfileInformation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
{
|
||||
FoldersOnly, // Only synchronize folder metadata.
|
||||
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.
|
||||
Full, // Synchronize everything
|
||||
Full, // Synchronize all folders. This won't update profile or alias information.
|
||||
UpdateProfile, // Only update profile information
|
||||
Alias, // Only update alias information
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
LanguageTimePage,
|
||||
AppPreferencesPage,
|
||||
SettingOptionsPage,
|
||||
AliasManagementPage
|
||||
}
|
||||
}
|
||||
|
||||
7
Wino.Core.Domain/Exceptions/MissingAliasException.cs
Normal file
7
Wino.Core.Domain/Exceptions/MissingAliasException.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Wino.Core.Domain.Exceptions
|
||||
{
|
||||
public class MissingAliasException : System.Exception
|
||||
{
|
||||
public MissingAliasException() : base(Translator.Exception_MissingAlias) { }
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
@@ -100,5 +101,59 @@ namespace Wino.Core.Domain.Interfaces
|
||||
/// </summary>
|
||||
/// <param name="accountIdOrderPair">AccountId-OrderNumber pair for all accounts.</param>
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
||||
using MailKit;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
|
||||
@@ -43,6 +44,13 @@ namespace Wino.Core.Domain.Interfaces
|
||||
/// <returns>Result summary of synchronization.</returns>
|
||||
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>
|
||||
/// Downloads a single MIME message from the server and saves it to disk.
|
||||
/// </summary>
|
||||
|
||||
9
Wino.Core.Domain/Interfaces/ICreateAccountAliasDialog.cs
Normal file
9
Wino.Core.Domain/Interfaces/ICreateAccountAliasDialog.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Wino.Core.Domain.Entities;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
public interface ICreateAccountAliasDialog
|
||||
{
|
||||
public MailAccountAlias CreatedAccountAlias { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -53,5 +53,6 @@ namespace Wino.Core.Domain.Interfaces
|
||||
/// </summary>
|
||||
/// <returns>Signature information. Null if canceled.</returns>
|
||||
Task<AccountSignature> ShowSignatureEditorDialog(AccountSignature signatureModel = null);
|
||||
Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
namespace Wino.Core.Domain.Models.Accounts
|
||||
{
|
||||
public record AccountCreationDialogResult(MailProviderType ProviderType, string AccountName, string SenderName, string AccountColorHex = "");
|
||||
public record AccountCreationDialogResult(MailProviderType ProviderType, string AccountName, string AccountColorHex = "");
|
||||
}
|
||||
|
||||
9
Wino.Core.Domain/Models/Accounts/ProfileInformation.cs
Normal file
9
Wino.Core.Domain/Models/Accounts/ProfileInformation.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Wino.Core.Domain.Models.Accounts
|
||||
{
|
||||
/// <summary>
|
||||
/// Encapsulates the profile information of an account.
|
||||
/// </summary>
|
||||
/// <param name="SenderName">Display sender name for the account.</param>
|
||||
/// <param name="Base64ProfilePictureData">Base 64 encoded profile picture data of the account. Thumbnail size.</param>
|
||||
public record ProfileInformation(string SenderName, string Base64ProfilePictureData);
|
||||
}
|
||||
@@ -14,7 +14,6 @@ namespace Wino.Core.Domain.Models.Accounts
|
||||
public string ProviderImage => $"ms-appx:///Assets/Providers/{Type}.png";
|
||||
|
||||
public bool IsSupported => Type == MailProviderType.Outlook || Type == MailProviderType.Gmail || Type == MailProviderType.IMAP4;
|
||||
public bool RequireSenderNameOnCreationDialog => Type != MailProviderType.IMAP4;
|
||||
|
||||
public ProviderDetail(MailProviderType type)
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace Wino.Core.Domain.Models.Authorization
|
||||
ClientId = clientId;
|
||||
|
||||
// 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,
|
||||
Uri.EscapeDataString(RedirectUri),
|
||||
ClientId,
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using MimeKit;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
|
||||
namespace Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
public class DraftPreparationRequest
|
||||
{
|
||||
public DraftPreparationRequest(MailAccount account, MailCopy createdLocalDraftCopy, string base64EncodedMimeMessage, MailCopy referenceMailCopy = null)
|
||||
public DraftPreparationRequest(MailAccount account,
|
||||
MailCopy createdLocalDraftCopy,
|
||||
string base64EncodedMimeMessage,
|
||||
DraftCreationReason reason,
|
||||
MailCopy referenceMailCopy = null)
|
||||
{
|
||||
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.
|
||||
|
||||
Base64LocalDraftMimeMessage = base64EncodedMimeMessage;
|
||||
Reason = reason;
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
@@ -29,6 +35,7 @@ public class DraftPreparationRequest
|
||||
public MailCopy ReferenceMailCopy { get; set; }
|
||||
|
||||
public string Base64LocalDraftMimeMessage { get; set; }
|
||||
public DraftCreationReason Reason { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
private MimeMessage createdLocalDraftMimeMessage;
|
||||
@@ -44,5 +51,5 @@ public class DraftPreparationRequest
|
||||
}
|
||||
}
|
||||
|
||||
public MailAccount Account { get; }
|
||||
public MailAccount Account { get; set; }
|
||||
}
|
||||
|
||||
@@ -5,45 +5,17 @@ using Wino.Core.Domain.Extensions;
|
||||
|
||||
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]
|
||||
private MimeMessage mime;
|
||||
|
||||
[JsonIgnore]
|
||||
public MimeMessage Mime
|
||||
{
|
||||
get
|
||||
{
|
||||
if (mime == null)
|
||||
{
|
||||
mime = Base64MimeMessage.GetMimeMessageFromBase64();
|
||||
}
|
||||
|
||||
return mime;
|
||||
}
|
||||
}
|
||||
public MimeMessage Mime => mime ??= Base64MimeMessage.GetMimeMessageFromBase64();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace Wino.Core.Domain.Models.Personalization
|
||||
{
|
||||
public record MailListPaneLengthPreferences(string Title, double Length);
|
||||
}
|
||||
@@ -3,14 +3,11 @@ using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Requests
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates request to queue and account for synchronizer.
|
||||
/// </summary>
|
||||
/// <param name="AccountId"><inheritdoc/></param>
|
||||
/// <param name="Request"></param>
|
||||
/// <param name="AccountId">Which account to execute this request for.</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 override string ToString() => $"Server Package: {Request.GetType().Name}";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
[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 static SynchronizationResult Empty => new() { CompletedState = SynchronizationCompletedState.Success };
|
||||
|
||||
public static SynchronizationResult Completed(IEnumerable<IMailItem> downloadedMessages)
|
||||
=> new() { DownloadedMessages = downloadedMessages, CompletedState = SynchronizationCompletedState.Success };
|
||||
public static SynchronizationResult Completed(IEnumerable<IMailItem> downloadedMessages, ProfileInformation profileInformation = null)
|
||||
=> new()
|
||||
{
|
||||
DownloadedMessages = downloadedMessages,
|
||||
ProfileInformation = profileInformation,
|
||||
CompletedState = SynchronizationCompletedState.Success
|
||||
};
|
||||
|
||||
public static SynchronizationResult Canceled => new() { CompletedState = SynchronizationCompletedState.Canceled };
|
||||
public static SynchronizationResult Failed => new() { CompletedState = SynchronizationCompletedState.Failed };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"AccountCreationDialog_Initializing": "initializing",
|
||||
"AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.",
|
||||
"AccountCreationDialog_SigninIn": "Account information is being saved.",
|
||||
"AccountCreationDialog_FetchingProfileInformation": "Fetching profile details.",
|
||||
"AccountEditDialog_Message": "Account Name",
|
||||
"AccountEditDialog_Title": "Edit Account",
|
||||
"AccountPickerDialog_Title": "Pick an account",
|
||||
@@ -21,6 +22,8 @@
|
||||
"BasicIMAPSetupDialog_Password": "Password",
|
||||
"BasicIMAPSetupDialog_Title": "IMAP Account",
|
||||
"Buttons_AddAccount": "Add Account",
|
||||
"Buttons_AddNewAlias": "Add New Alias",
|
||||
"Buttons_SyncAliases": "Synchronize Aliases",
|
||||
"Buttons_ApplyTheme": "Apply Theme",
|
||||
"Buttons_Browse": "Browse",
|
||||
"Buttons_Cancel": "Cancel",
|
||||
@@ -42,6 +45,7 @@
|
||||
"Buttons_SignIn": "Sign In",
|
||||
"Buttons_TryAgain": "Try Again",
|
||||
"Buttons_Yes": "Yes",
|
||||
"Buttons_Reset": "Reset",
|
||||
"Center": "Center",
|
||||
"ComingSoon": "Coming soon...",
|
||||
"ComposerFrom": "From: ",
|
||||
@@ -64,6 +68,16 @@
|
||||
"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_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_CleanupFolderTitle": "Cleanup Folder",
|
||||
"DialogMessage_ComposerMissingRecipientMessage": "Message has no recipient.",
|
||||
@@ -92,9 +106,16 @@
|
||||
"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}.",
|
||||
"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.",
|
||||
"DiscordChannelDisclaimerTitle": "Important Discord Information",
|
||||
"Draft": "Draft",
|
||||
"Busy": "Busy",
|
||||
"EditorToolbarOption_Draw": "Draw",
|
||||
"EditorToolbarOption_Format": "Format",
|
||||
"EditorToolbarOption_Insert": "Insert",
|
||||
@@ -106,6 +127,7 @@
|
||||
"ElementTheme_Light": "Light mode",
|
||||
"Emoji": "Emoji",
|
||||
"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_ImapClientPoolFailed": "IMAP Client Pool failed.",
|
||||
"Exception_AuthenticationCanceled": "Authentication canceled",
|
||||
@@ -113,6 +135,9 @@
|
||||
"Exception_CustomThemeMissingName": "You must provide a name.",
|
||||
"Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.",
|
||||
"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_GoogleAuthCorruptedCode": "Corrupted authorization response.",
|
||||
"Exception_GoogleAuthError": "OAuth authorization error: {0}",
|
||||
@@ -253,6 +278,8 @@
|
||||
"Info_UnsubscribeLinkInvalidMessage": "This unsubscribe link is invalid. Failed to unsubscribe from the list.",
|
||||
"Info_UnsubscribeSuccessMessage": "Successfully unsubscribed from {0}.",
|
||||
"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_ConnectionSecurity": "Connection security",
|
||||
"ImapAuthenticationMethod_Auto": "Auto",
|
||||
@@ -396,6 +423,8 @@
|
||||
"SettingsFolderSync_Title": "Folder Synchronization",
|
||||
"SettingsFolderOptions_Title": "Folder Configuration",
|
||||
"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",
|
||||
"SettingsHoverActionLeft": "Left Action",
|
||||
"SettingsHoverActionRight": "Right Action",
|
||||
@@ -406,6 +435,11 @@
|
||||
"SettingsLanguageTime_Title": "Language & Time",
|
||||
"SettingsLanguageTime_Description": "Wino display language, preferred time format.",
|
||||
"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",
|
||||
"SettingsOptions_Title": "Settings",
|
||||
"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.",
|
||||
"SettingsNotifications_Description": "Turn on or off notifications for this account.",
|
||||
"SettingsNotifications_Title": "Notifications",
|
||||
"SettingsPaneLength_Description": "Change the width of the mail list.",
|
||||
"SettingsPaneLength_Title": "Mail List Pane Length",
|
||||
"SettingsPaneLengthReset_Description": "Reset the size of the mail list to original if you have issues with it.",
|
||||
"SettingsPaneLengthReset_Title": "Reset Mail List Size",
|
||||
"SettingsPaypal_Description": "Show much more love ❤️ All donations are appreciated.",
|
||||
"SettingsPaypal_Title": "Donate via PayPal",
|
||||
"SettingsPersonalizationMailDisplayCompactMode": "Compact Mode",
|
||||
|
||||
180
Wino.Core.Domain/Translator.Designer.cs
generated
180
Wino.Core.Domain/Translator.Designer.cs
generated
@@ -38,6 +38,11 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
public static string AccountCreationDialog_SigninIn => Resources.GetTranslatedString(@"AccountCreationDialog_SigninIn");
|
||||
|
||||
/// <summary>
|
||||
/// Fetching profile details.
|
||||
/// </summary>
|
||||
public static string AccountCreationDialog_FetchingProfileInformation => Resources.GetTranslatedString(@"AccountCreationDialog_FetchingProfileInformation");
|
||||
|
||||
/// <summary>
|
||||
/// Account Name
|
||||
/// </summary>
|
||||
@@ -128,6 +133,16 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
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>
|
||||
/// Apply Theme
|
||||
/// </summary>
|
||||
@@ -233,6 +248,11 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
public static string Buttons_Yes => Resources.GetTranslatedString(@"Buttons_Yes");
|
||||
|
||||
/// <summary>
|
||||
/// Reset
|
||||
/// </summary>
|
||||
public static string Buttons_Reset => Resources.GetTranslatedString(@"Buttons_Reset");
|
||||
|
||||
/// <summary>
|
||||
/// Center
|
||||
/// </summary>
|
||||
@@ -343,6 +363,56 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
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>
|
||||
/// Do you want to permanently delete all the mails in this folder?
|
||||
/// </summary>
|
||||
@@ -434,7 +504,7 @@ namespace Wino.Core.Domain
|
||||
public static string DialogMessage_UnlinkAccountsConfirmationTitle => Resources.GetTranslatedString(@"DialogMessage_UnlinkAccountsConfirmationTitle");
|
||||
|
||||
/// <summary>
|
||||
/// Missin Subject
|
||||
/// Missing Subject
|
||||
/// </summary>
|
||||
public static string DialogMessage_EmptySubjectConfirmation => Resources.GetTranslatedString(@"DialogMessage_EmptySubjectConfirmation");
|
||||
|
||||
@@ -483,6 +553,36 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
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>
|
||||
/// 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>
|
||||
@@ -498,6 +598,11 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
public static string Draft => Resources.GetTranslatedString(@"Draft");
|
||||
|
||||
/// <summary>
|
||||
/// Busy
|
||||
/// </summary>
|
||||
public static string Busy => Resources.GetTranslatedString(@"Busy");
|
||||
|
||||
/// <summary>
|
||||
/// Draw
|
||||
/// </summary>
|
||||
@@ -553,6 +658,11 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
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>
|
||||
/// Couldn't find mailbox settings.
|
||||
/// </summary>
|
||||
@@ -588,6 +698,21 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
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>
|
||||
/// Callback uri is null on activation.
|
||||
/// </summary>
|
||||
@@ -1288,6 +1413,16 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
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>
|
||||
/// Authentication method
|
||||
/// </summary>
|
||||
@@ -2003,6 +2138,16 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
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>
|
||||
/// Center Action
|
||||
/// </summary>
|
||||
@@ -2053,6 +2198,31 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
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>
|
||||
/// More
|
||||
/// </summary>
|
||||
@@ -2264,14 +2434,14 @@ namespace Wino.Core.Domain
|
||||
public static string SettingsNotifications_Title => Resources.GetTranslatedString(@"SettingsNotifications_Title");
|
||||
|
||||
/// <summary>
|
||||
/// Change the width of the mail list.
|
||||
/// Reset the size of the mail list to original if you have issues with it.
|
||||
/// </summary>
|
||||
public static string SettingsPaneLength_Description => Resources.GetTranslatedString(@"SettingsPaneLength_Description");
|
||||
public static string SettingsPaneLengthReset_Description => Resources.GetTranslatedString(@"SettingsPaneLengthReset_Description");
|
||||
|
||||
/// <summary>
|
||||
/// Mail List Pane Length
|
||||
/// Reset Mail List Size
|
||||
/// </summary>
|
||||
public static string SettingsPaneLength_Title => Resources.GetTranslatedString(@"SettingsPaneLength_Title");
|
||||
public static string SettingsPaneLengthReset_Title => Resources.GetTranslatedString(@"SettingsPaneLengthReset_Title");
|
||||
|
||||
/// <summary>
|
||||
/// Show much more love ❤️ All donations are appreciated.
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Wino.Core.Authenticators
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.MenuItems;
|
||||
|
||||
namespace Wino.Core.Extensions
|
||||
{
|
||||
public static class FolderTreeExtensions
|
||||
{
|
||||
private static MenuItemBase<IMailItemFolder, FolderMenuItem> GetMenuItemByFolderRecursive(IMailItemFolder structure, AccountMenuItem parentAccountMenuItem, IMenuItem parentFolderItem)
|
||||
{
|
||||
MenuItemBase<IMailItemFolder, FolderMenuItem> parentMenuItem = new FolderMenuItem(structure, parentAccountMenuItem.Parameter, parentFolderItem);
|
||||
|
||||
var childStructures = structure.ChildFolders;
|
||||
|
||||
foreach (var childFolder in childStructures)
|
||||
{
|
||||
if (childFolder == null) continue;
|
||||
|
||||
// Folder menu item.
|
||||
var subChildrenFolderTree = GetMenuItemByFolderRecursive(childFolder, parentAccountMenuItem, parentMenuItem);
|
||||
|
||||
if (subChildrenFolderTree is FolderMenuItem folderItem)
|
||||
{
|
||||
parentMenuItem.SubMenuItems.Add(folderItem);
|
||||
}
|
||||
}
|
||||
|
||||
return parentMenuItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
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();
|
||||
|
||||
if (mimeMessage == null)
|
||||
return response?.SendAs?.Select(a => new RemoteAccountAlias()
|
||||
{
|
||||
// This should never happen.
|
||||
Debugger.Break();
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
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);
|
||||
AliasAddress = a.SendAsEmail,
|
||||
IsRootAlias = a.IsDefault.GetValueOrDefault(),
|
||||
IsPrimary = a.IsPrimary.GetValueOrDefault(),
|
||||
ReplyToAddress = a.ReplyToAddress,
|
||||
IsVerified = a.VerificationStatus == "accepted" || a.IsDefault.GetValueOrDefault(),
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Graph.Models;
|
||||
using MimeKit;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
@@ -61,5 +65,160 @@ namespace Wino.Core.Extensions
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,13 @@ namespace Wino.Core.Integration
|
||||
// Later on maybe we can make it configurable and leave it to the user with passing
|
||||
// real implementation details.
|
||||
|
||||
private readonly ImapImplementation _implementation = new ImapImplementation()
|
||||
private readonly ImapImplementation _implementation = new()
|
||||
{
|
||||
Version = "1.0",
|
||||
Version = "1.8.0",
|
||||
OS = "Windows",
|
||||
Vendor = "Wino"
|
||||
Vendor = "Wino",
|
||||
SupportUrl = "https://www.winomail.app",
|
||||
Name = "Wino Mail User",
|
||||
};
|
||||
|
||||
private readonly int MinimumPoolSize = 5;
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace Wino.Core.Integration.Processors
|
||||
/// </summary>
|
||||
public interface IDefaultChangeProcessor
|
||||
{
|
||||
Task UpdateAccountAsync(MailAccount account);
|
||||
Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier);
|
||||
Task CreateAssignmentAsync(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>
|
||||
Task<List<MailItemFolder>> GetLocalFoldersAsync(Guid accountId);
|
||||
|
||||
|
||||
Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(SynchronizationOptions options);
|
||||
|
||||
Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
|
||||
Task UpdateFolderLastSyncDateAsync(Guid folderId);
|
||||
|
||||
Task<List<MailItemFolder>> GetExistingFoldersAsync(Guid accountId);
|
||||
Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases);
|
||||
}
|
||||
|
||||
public interface IGmailChangeProcessor : IDefaultChangeProcessor
|
||||
@@ -172,5 +175,11 @@ namespace Wino.Core.Integration.Processors
|
||||
|
||||
public Task UpdateFolderLastSyncDateAsync(Guid folderId)
|
||||
=> FolderService.UpdateFolderLastSyncDateAsync(folderId);
|
||||
|
||||
public Task UpdateAccountAsync(MailAccount account)
|
||||
=> AccountService.UpdateAccountAsync(account);
|
||||
|
||||
public Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases)
|
||||
=> AccountService.UpdateRemoteAliasInformationAsync(account, remoteAccountAliases);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,12 @@ namespace Wino.Core.MenuItems
|
||||
set => SetProperty(Parameter.Name, value, Parameter, (u, n) => u.Name = n);
|
||||
}
|
||||
|
||||
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 AccountMenuItem(MailAccount account, IMenuItem parent = null) : base(account, account.Id, parent)
|
||||
@@ -59,6 +65,7 @@ namespace Wino.Core.MenuItems
|
||||
Parameter = account;
|
||||
AccountName = account.Name;
|
||||
AttentionReason = account.AttentionReason;
|
||||
Base64ProfilePicture = account.Base64ProfilePictureData;
|
||||
|
||||
if (SubMenuItems == null) return;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Diagnostics;
|
||||
@@ -10,6 +9,7 @@ using SqlKata;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Messaging.Client.Accounts;
|
||||
using Wino.Messaging.UI;
|
||||
@@ -233,6 +233,33 @@ namespace Wino.Core.Services
|
||||
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)
|
||||
=> 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<MailItemFolder>().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.
|
||||
// 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));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (account.Preferences == null)
|
||||
{
|
||||
Debugger.Break();
|
||||
}
|
||||
|
||||
await Connection.UpdateAsync(account.Preferences);
|
||||
await Connection.UpdateAsync(account);
|
||||
await Connection.UpdateAsync(account.Preferences).ConfigureAwait(false);
|
||||
await Connection.UpdateAsync(account).ConfigureAwait(false);
|
||||
|
||||
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)
|
||||
{
|
||||
Guard.IsNotNull(account);
|
||||
@@ -385,7 +507,7 @@ namespace Wino.Core.Services
|
||||
// Outlook token cache is managed by MSAL.
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -437,5 +559,14 @@ namespace Wino.Core.Services
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ namespace Wino.Core.Services
|
||||
typeof(CustomServerInformation),
|
||||
typeof(AccountSignature),
|
||||
typeof(MergedInbox),
|
||||
typeof(MailAccountPreferences)
|
||||
typeof(MailAccountPreferences),
|
||||
typeof(MailAccountAlias)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using SqlKata;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Comparers;
|
||||
@@ -62,12 +63,14 @@ namespace Wino.Core.Services
|
||||
// This header will be used to map the local draft copy with the remote draft copy.
|
||||
var mimeUniqueId = createdDraftMimeMessage.Headers[Constants.WinoLocalDraftHeader];
|
||||
|
||||
var primaryAlias = await _accountService.GetPrimaryAccountAliasAsync(accountId).ConfigureAwait(false);
|
||||
|
||||
var copy = new MailCopy
|
||||
{
|
||||
UniqueId = Guid.Parse(mimeUniqueId),
|
||||
Id = Guid.NewGuid().ToString(), // This will be replaced after network call with the remote draft id.
|
||||
CreationDate = DateTime.UtcNow,
|
||||
FromAddress = composerAccount.Address,
|
||||
FromAddress = primaryAlias?.AliasAddress ?? composerAccount.Address,
|
||||
FromName = composerAccount.SenderName,
|
||||
HasAttachments = false,
|
||||
Importance = MailImportance.Normal,
|
||||
@@ -621,12 +624,17 @@ namespace Wino.Core.Services
|
||||
// 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.
|
||||
// Synchronizer will map this unique id to the local draft copy after synchronization.
|
||||
|
||||
var message = new MimeMessage()
|
||||
{
|
||||
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 signature = await GetSignature(account, draftCreationOptions.Reason);
|
||||
@@ -729,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)));
|
||||
}
|
||||
|
||||
// Self email can be present at this step, when replying to own message. It should be removed only in case there no other recipients.
|
||||
if (message.To.Count > 1)
|
||||
{
|
||||
var self = message.To.FirstOrDefault(x => x is MailboxAddress mailboxAddress && mailboxAddress.Address.Equals(account.Address, StringComparison.OrdinalIgnoreCase));
|
||||
if (self != null)
|
||||
message.To.Remove(self);
|
||||
}
|
||||
|
||||
// Manage "ThreadId-ConversationId"
|
||||
if (!string.IsNullOrEmpty(referenceMessage.MessageId))
|
||||
{
|
||||
|
||||
@@ -109,12 +109,9 @@ namespace Wino.Core.Services
|
||||
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId).ConfigureAwait(false);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
@@ -12,6 +13,7 @@ using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Integration;
|
||||
@@ -69,8 +71,65 @@ namespace Wino.Core.Synchronizers
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
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)
|
||||
{
|
||||
try
|
||||
@@ -104,6 +163,48 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
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.
|
||||
|
||||
bool shouldDelayExecution = batches.Any(a => a.DelayExecution);
|
||||
@@ -150,6 +251,10 @@ namespace Wino.Core.Synchronizers
|
||||
private void PublishUnreadItemChanges()
|
||||
=> 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)
|
||||
=> WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress));
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using Google.Apis.Gmail.v1;
|
||||
using Google.Apis.Gmail.v1.Data;
|
||||
using Google.Apis.Http;
|
||||
using Google.Apis.PeopleService.v1;
|
||||
using Google.Apis.Requests;
|
||||
using Google.Apis.Services;
|
||||
using MailKit;
|
||||
@@ -18,6 +19,7 @@ using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
@@ -37,8 +39,10 @@ namespace Wino.Core.Synchronizers
|
||||
// https://github.com/googleapis/google-api-dotnet-client/issues/2603
|
||||
private const uint MaximumAllowedBatchRequestSize = 10;
|
||||
|
||||
private readonly ConfigurableHttpClient _gmailHttpClient;
|
||||
private readonly ConfigurableHttpClient _googleHttpClient;
|
||||
private readonly GmailService _gmailService;
|
||||
private readonly PeopleServiceService _peopleService;
|
||||
|
||||
private readonly IAuthenticator _authenticator;
|
||||
private readonly IGmailChangeProcessor _gmailChangeProcessor;
|
||||
private readonly ILogger _logger = Log.ForContext<GmailSynchronizer>();
|
||||
@@ -54,15 +58,48 @@ namespace Wino.Core.Synchronizers
|
||||
HttpClientFactory = this
|
||||
};
|
||||
|
||||
_gmailHttpClient = new ConfigurableHttpClient(messageHandler);
|
||||
_googleHttpClient = new ConfigurableHttpClient(messageHandler);
|
||||
|
||||
_gmailService = new GmailService(initializer);
|
||||
_peopleService = new PeopleServiceService(initializer);
|
||||
|
||||
_authenticator = authenticator;
|
||||
_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);
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -18,11 +18,11 @@ using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
|
||||
using MimeKit;
|
||||
using MoreLinq.Extensions;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
@@ -128,7 +128,7 @@ namespace Wino.Core.Synchronizers
|
||||
#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>();
|
||||
|
||||
@@ -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
|
||||
|
||||
public override bool DelaySendOperationSynchronization() => true;
|
||||
@@ -572,22 +606,37 @@ namespace Wino.Core.Synchronizers
|
||||
{
|
||||
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());
|
||||
var base64Encoded = Convert.ToBase64String(plainTextBytes);
|
||||
|
||||
var requestInformation = _graphClient.Me.Messages.ToPostRequestInformation(new Message());
|
||||
|
||||
requestInformation.Headers.Clear();// replace the json content header
|
||||
requestInformation.Headers.Add("Content-Type", "text/plain");
|
||||
|
||||
requestInformation.SetStreamContent(new MemoryStream(Encoding.UTF8.GetBytes(base64Encoded)), "text/plain");
|
||||
|
||||
return requestInformation;
|
||||
if (reason == DraftCreationReason.Empty)
|
||||
{
|
||||
return _graphClient.Me.Messages.ToPostRequestInformation(message);
|
||||
}
|
||||
else if (reason == DraftCreationReason.Reply)
|
||||
{
|
||||
return _graphClient.Me.Messages[createDraftRequest.DraftPreperationRequest.ReferenceMailCopy.Id].CreateReply.ToPostRequestInformation(new Microsoft.Graph.Me.Messages.Item.CreateReply.CreateReplyPostRequestBody()
|
||||
{
|
||||
Message = message
|
||||
});
|
||||
}
|
||||
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 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 base64Encoded = Convert.ToBase64String(plainTextBytes);
|
||||
var sendDraftRequestBundle = new HttpRequestBundle<RequestInformation>(sendDraftRequest, request);
|
||||
|
||||
var outlookMessage = new Message()
|
||||
{
|
||||
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];
|
||||
return [patchDraftRequestBundle, sendDraftRequestBundle];
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<RequestInformation>> Archive(BatchArchiveRequest request)
|
||||
=> Move(new BatchMoveRequest(request.Items, request.FromFolder, request.ToFolder));
|
||||
|
||||
|
||||
|
||||
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem,
|
||||
MailKit.ITransferProgress transferProgress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -697,26 +739,41 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
request.ApplyUIChanges();
|
||||
|
||||
await batchContent.AddBatchRequestStepAsync(nativeRequest).ConfigureAwait(false);
|
||||
var batchRequestId = await batchContent.AddBatchRequestStepAsync(nativeRequest).ConfigureAwait(false);
|
||||
|
||||
// Map BundleId to batch request step's key.
|
||||
// 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())
|
||||
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.
|
||||
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.
|
||||
// Each bundle id must return some HttpResponseMessage ideally.
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -727,45 +784,31 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
var httpResponseMessage = await batchRequestResponse.GetResponseByIdAsync(bundleId);
|
||||
|
||||
if (httpResponseMessage == null)
|
||||
continue;
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
<PackageReference Include="CommunityToolkit.Diagnostics" 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="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="IsExternalInit" Version="1.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@@ -26,9 +27,9 @@
|
||||
<PackageReference Include="MailKit" Version="4.7.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Graph" Version="5.56.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.62.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.62.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.62.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.63.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.63.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.63.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.7.1" />
|
||||
<PackageReference Include="morelinq" Version="4.1.0" />
|
||||
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
|
||||
|
||||
@@ -56,7 +56,11 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
[RelayCommand]
|
||||
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)
|
||||
=> _folderService.ChangeFolderSynchronizationStateAsync(folderStructure.Id, isEnabled);
|
||||
|
||||
@@ -154,15 +154,12 @@ namespace Wino.Mail.ViewModels
|
||||
{
|
||||
creationDialog = _dialogService.GetAccountCreationDialog(accountCreationDialogResult.ProviderType);
|
||||
|
||||
// _accountService.ExternalAuthenticationAuthenticator = _authenticationProvider.GetAuthenticator(accountCreationDialogResult.ProviderType);
|
||||
|
||||
CustomServerInformation customServerInformation = null;
|
||||
|
||||
createdAccount = new MailAccount()
|
||||
{
|
||||
ProviderType = accountCreationDialogResult.ProviderType,
|
||||
Name = accountCreationDialogResult.AccountName,
|
||||
SenderName = accountCreationDialogResult.SenderName,
|
||||
AccountColorHex = accountCreationDialogResult.AccountColorHex,
|
||||
Id = Guid.NewGuid()
|
||||
};
|
||||
@@ -208,30 +205,83 @@ namespace Wino.Mail.ViewModels
|
||||
await _accountService.CreateAccountAsync(createdAccount, tokenInformation, customServerInformation);
|
||||
|
||||
// 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)
|
||||
customServerAccountCreationDialog.ShowPreparingFolders();
|
||||
else
|
||||
creationDialog.State = AccountCreationDialogState.PreparingFolders;
|
||||
|
||||
var options = new SynchronizationOptions()
|
||||
// Start synchronizing folders.
|
||||
var folderSyncOptions = new SynchronizationOptions()
|
||||
{
|
||||
AccountId = createdAccount.Id,
|
||||
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;
|
||||
if (synchronizationResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
var folderSynchronizationResult = folderSynchronizationResponse.Data;
|
||||
|
||||
if (folderSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
|
||||
|
||||
// Check if Inbox folder is available for the account after synchronization.
|
||||
var isInboxAvailable = await _folderService.IsInboxAvailableForAccountAsync(createdAccount.Id);
|
||||
// Sync aliases if supported.
|
||||
if (createdAccount.IsAliasSyncSupported)
|
||||
{
|
||||
// Try to synchronize aliases for the account.
|
||||
|
||||
if (!isInboxAvailable)
|
||||
throw new Exception(Translator.Exception_InboxNotAvailable);
|
||||
var aliasSyncOptions = new SynchronizationOptions()
|
||||
{
|
||||
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.
|
||||
ReportUIChange(new AccountCreatedMessage(createdAccount));
|
||||
|
||||
143
Wino.Mail.ViewModels/AliasManagementPageViewModel.cs
Normal file
143
Wino.Mail.ViewModels/AliasManagementPageViewModel.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using EmailValidation;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Messaging.Server;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
public partial class AliasManagementPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CanSynchronizeAliases))]
|
||||
private MailAccount account;
|
||||
|
||||
[ObservableProperty]
|
||||
private List<MailAccountAlias> accountAliases = [];
|
||||
|
||||
public bool CanSynchronizeAliases => Account?.IsAliasSyncSupported ?? false;
|
||||
|
||||
public AliasManagementPageViewModel(IDialogService dialogService,
|
||||
IAccountService accountService,
|
||||
IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_winoServerConnectionManager = winoServerConnectionManager;
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
if (parameters is Guid accountId)
|
||||
Account = await _accountService.GetAccountAsync(accountId);
|
||||
|
||||
if (Account == null) return;
|
||||
|
||||
await LoadAliasesAsync();
|
||||
}
|
||||
|
||||
private async Task LoadAliasesAsync()
|
||||
{
|
||||
AccountAliases = await _accountService.GetAccountAliasesAsync(Account.Id);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SetAliasPrimaryAsync(MailAccountAlias alias)
|
||||
{
|
||||
if (alias.IsPrimary) return;
|
||||
|
||||
AccountAliases.ForEach(a =>
|
||||
{
|
||||
a.IsPrimary = a == alias;
|
||||
});
|
||||
|
||||
await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases);
|
||||
await LoadAliasesAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SyncAliasesAsync()
|
||||
{
|
||||
if (!CanSynchronizeAliases) return;
|
||||
|
||||
var aliasSyncOptions = new SynchronizationOptions()
|
||||
{
|
||||
AccountId = Account.Id,
|
||||
Type = SynchronizationType.Alias
|
||||
};
|
||||
|
||||
var aliasSyncResponse = await _winoServerConnectionManager.GetResponseAsync<SynchronizationResult, NewSynchronizationRequested>(new NewSynchronizationRequested(aliasSyncOptions, SynchronizationSource.Client));
|
||||
|
||||
if (aliasSyncResponse.IsSuccess)
|
||||
await LoadAliasesAsync();
|
||||
else
|
||||
DialogService.InfoBarMessage(Translator.GeneralTitle_Error, aliasSyncResponse.Message, InfoBarMessageType.Error);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddNewAliasAsync()
|
||||
{
|
||||
var createdAliasDialog = await DialogService.ShowCreateAccountAliasDialogAsync();
|
||||
|
||||
if (createdAliasDialog.CreatedAccountAlias == null) return;
|
||||
|
||||
var newAlias = createdAliasDialog.CreatedAccountAlias;
|
||||
|
||||
// Check existence.
|
||||
if (AccountAliases.Any(a => a.AliasAddress == newAlias.AliasAddress))
|
||||
{
|
||||
await DialogService.ShowMessageAsync(Translator.DialogMessage_AliasExistsTitle, Translator.DialogMessage_AliasExistsMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate all addresses.
|
||||
if (!EmailValidator.Validate(newAlias.AliasAddress) || (!string.IsNullOrEmpty(newAlias.ReplyToAddress) && !EmailValidator.Validate(newAlias.ReplyToAddress)))
|
||||
{
|
||||
await DialogService.ShowMessageAsync(Translator.DialogMessage_InvalidAliasMessage, Translator.DialogMessage_InvalidAliasTitle);
|
||||
return;
|
||||
}
|
||||
|
||||
newAlias.AccountId = Account.Id;
|
||||
|
||||
AccountAliases.Add(newAlias);
|
||||
|
||||
await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases);
|
||||
DialogService.InfoBarMessage(Translator.DialogMessage_AliasCreatedTitle, Translator.DialogMessage_AliasCreatedMessage, InfoBarMessageType.Success);
|
||||
|
||||
await LoadAliasesAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteAliasAsync(MailAccountAlias alias)
|
||||
{
|
||||
// Primary aliases can't be deleted.
|
||||
if (alias.IsPrimary)
|
||||
{
|
||||
await DialogService.ShowMessageAsync(Translator.Info_CantDeletePrimaryAliasMessage, Translator.GeneralTitle_Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// Root aliases can't be deleted.
|
||||
if (alias.IsRootAlias)
|
||||
{
|
||||
await DialogService.ShowMessageAsync(Translator.DialogMessage_CantDeleteRootAliasTitle, Translator.DialogMessage_CantDeleteRootAliasMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
await _accountService.DeleteAccountAliasAsync(alias.Id);
|
||||
await LoadAliasesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -729,17 +729,26 @@ namespace Wino.Mail.ViewModels
|
||||
operationAccount = accounts.FirstOrDefault();
|
||||
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)
|
||||
{
|
||||
// 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);
|
||||
|
||||
if (!mergedAccounts.Any()) return;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,7 +788,7 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -795,7 +804,7 @@ namespace Wino.Mail.ViewModels
|
||||
}
|
||||
|
||||
protected override void OnAccountRemoved(MailAccount removedAccount)
|
||||
=> Messenger.Send(new AccountsMenuRefreshRequested(true));
|
||||
=> Messenger.Send(new AccountsMenuRefreshRequested(false));
|
||||
|
||||
protected override async void OnAccountCreated(MailAccount createdAccount)
|
||||
{
|
||||
@@ -877,12 +886,9 @@ namespace Wino.Mail.ViewModels
|
||||
{
|
||||
await RecreateMenuItemsAsync();
|
||||
|
||||
if (message.AutomaticallyNavigateFirstItem)
|
||||
if (MenuItems.FirstOrDefault(a => a is IAccountMenuItem) is IAccountMenuItem firstAccount)
|
||||
{
|
||||
if (MenuItems.FirstOrDefault(a => a is IAccountMenuItem) is IAccountMenuItem firstAccount)
|
||||
{
|
||||
await ChangeLoadedAccountAsync(firstAccount);
|
||||
}
|
||||
await ChangeLoadedAccountAsync(firstAccount, message.AutomaticallyNavigateFirstItem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using MimeKit;
|
||||
using MimeKit.Utils;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
@@ -21,6 +20,7 @@ using Wino.Core.Extensions;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Messaging.Client.Mails;
|
||||
using Wino.Messaging.Server;
|
||||
|
||||
namespace Wino.Mail.ViewModels
|
||||
{
|
||||
@@ -68,6 +68,12 @@ namespace Wino.Mail.ViewModels
|
||||
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
|
||||
private MailAccount composingAccount;
|
||||
|
||||
[ObservableProperty]
|
||||
private List<MailAccountAlias> availableAliases;
|
||||
|
||||
[ObservableProperty]
|
||||
private MailAccountAlias selectedAlias;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isDraggingOverComposerGrid;
|
||||
|
||||
@@ -112,6 +118,7 @@ namespace Wino.Mail.ViewModels
|
||||
private readonly IWinoRequestDelegator _worker;
|
||||
public readonly IFontService FontService;
|
||||
public readonly IPreferencesService PreferencesService;
|
||||
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
|
||||
public readonly IContactService ContactService;
|
||||
|
||||
public ComposePageViewModel(IDialogService dialogService,
|
||||
@@ -124,21 +131,23 @@ namespace Wino.Mail.ViewModels
|
||||
IWinoRequestDelegator worker,
|
||||
IContactService contactService,
|
||||
IFontService fontService,
|
||||
IPreferencesService preferencesService) : base(dialogService)
|
||||
IPreferencesService preferencesService,
|
||||
IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService)
|
||||
{
|
||||
NativeAppService = nativeAppService;
|
||||
_folderService = folderService;
|
||||
ContactService = contactService;
|
||||
FontService = fontService;
|
||||
PreferencesService = preferencesService;
|
||||
|
||||
_folderService = folderService;
|
||||
_mailService = mailService;
|
||||
_launchProtocolService = launchProtocolService;
|
||||
_mimeFileService = mimeFileService;
|
||||
_accountService = accountService;
|
||||
_worker = worker;
|
||||
_winoServerConnectionManager = winoServerConnectionManager;
|
||||
|
||||
SelectedToolbarSection = ToolbarSections[0];
|
||||
PreferencesService = preferencesService;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -163,6 +172,12 @@ namespace Wino.Mail.ViewModels
|
||||
if (!isConfirmed) return;
|
||||
}
|
||||
|
||||
if (SelectedAlias == null)
|
||||
{
|
||||
DialogService.InfoBarMessage(Translator.DialogMessage_AliasNotSelectedTitle, Translator.DialogMessage_AliasNotSelectedMessage, InfoBarMessageType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save mime changes before sending.
|
||||
await UpdateMimeChangesAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -177,11 +192,26 @@ namespace Wino.Mail.ViewModels
|
||||
int count = (int)memoryStream.Length;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if (isUpdatingMimeBlocked || CurrentMimeMessage == null || ComposingAccount == null || CurrentMailDraftItem == null) return;
|
||||
@@ -194,6 +224,8 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
SaveImportance();
|
||||
SaveSubject();
|
||||
SaveFromAddress();
|
||||
SaveReplyToAddress();
|
||||
|
||||
await SaveAttachmentsAsync();
|
||||
await SaveBodyAsync();
|
||||
@@ -207,6 +239,8 @@ namespace Wino.Mail.ViewModels
|
||||
{
|
||||
CurrentMailDraftItem.Subject = CurrentMimeMessage.Subject;
|
||||
CurrentMailDraftItem.PreviewText = CurrentMimeMessage.TextBody;
|
||||
CurrentMailDraftItem.FromAddress = SelectedAlias.AliasAddress;
|
||||
CurrentMailDraftItem.HasAttachments = CurrentMimeMessage.Attachments.Any();
|
||||
|
||||
// Update database.
|
||||
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()
|
||||
{
|
||||
@@ -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()
|
||||
{
|
||||
if (GetHTMLBodyFunction != null)
|
||||
@@ -241,8 +303,7 @@ namespace Wino.Mail.ViewModels
|
||||
bodyBuilder.SetHtmlBody(await GetHTMLBodyFunction());
|
||||
}
|
||||
|
||||
if (bodyBuilder.HtmlBody != null && bodyBuilder.TextBody != null)
|
||||
CurrentMimeMessage.Body = bodyBuilder.ToMessageBody();
|
||||
CurrentMimeMessage.Body = bodyBuilder.ToMessageBody();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(canSendMail))]
|
||||
@@ -280,6 +341,8 @@ namespace Wino.Mail.ViewModels
|
||||
base.OnNavigatedFrom(mode, parameters);
|
||||
|
||||
await UpdateMimeChangesAsync().ConfigureAwait(false);
|
||||
|
||||
Messenger.Send(new KillChromiumRequested());
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
@@ -288,92 +351,58 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
if (parameters != null && parameters is MailItemViewModel mailItem)
|
||||
{
|
||||
await LoadAccountsAsync();
|
||||
|
||||
CurrentMailDraftItem = mailItem;
|
||||
|
||||
_ = 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);
|
||||
await TryPrepareComposeAsync(true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> InitializeComposerAccountAsync()
|
||||
{
|
||||
if (CurrentMailDraftItem == null) return false;
|
||||
|
||||
if (ComposingAccount != null) return true;
|
||||
|
||||
if (CurrentMailDraftItem == null)
|
||||
return false;
|
||||
var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.AssignedAccount.Id).ConfigureAwait(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(() =>
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (CurrentMailDraftItem == null)
|
||||
return;
|
||||
if (CurrentMailDraftItem == null) return;
|
||||
|
||||
bool isComposerInitialized = await InitializeComposerAccountAsync();
|
||||
|
||||
if (!isComposerInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!isComposerInitialized) return;
|
||||
|
||||
retry:
|
||||
|
||||
// Replying existing message.
|
||||
MimeMessageInformation mimeMessageInformation = null;
|
||||
@@ -386,18 +415,24 @@ namespace Wino.Mail.ViewModels
|
||||
{
|
||||
if (downloadIfNeeded)
|
||||
{
|
||||
// TODO: Folder id needs to be passed.
|
||||
// TODO: Send mail retrieve request.
|
||||
// _worker.Queue(new FetchSingleItemRequest(ComposingAccount.Id, CurrentMailDraftItem.Id, string.Empty));
|
||||
downloadIfNeeded = false;
|
||||
|
||||
var package = new DownloadMissingMessageRequested(CurrentMailDraftItem.AssignedAccount.Id, CurrentMailDraftItem.MailCopy);
|
||||
var downloadResponse = await _winoServerConnectionManager.GetResponseAsync<bool, DownloadMissingMessageRequested>(package);
|
||||
|
||||
if (downloadResponse.IsSuccess)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
//else
|
||||
// DialogService.ShowMIMENotFoundMessage();
|
||||
else
|
||||
DialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error);
|
||||
|
||||
return;
|
||||
}
|
||||
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)
|
||||
{
|
||||
@@ -416,6 +451,8 @@ namespace Wino.Mail.ViewModels
|
||||
{
|
||||
// Extract information
|
||||
|
||||
CurrentMimeMessage = replyingMime;
|
||||
|
||||
ToItems.Clear();
|
||||
CCItems.Clear();
|
||||
BCCItems.Clear();
|
||||
@@ -424,22 +461,22 @@ namespace Wino.Mail.ViewModels
|
||||
LoadAddressInfo(replyingMime.Cc, CCItems);
|
||||
LoadAddressInfo(replyingMime.Bcc, BCCItems);
|
||||
|
||||
LoadAttachments(replyingMime.Attachments);
|
||||
LoadAttachments();
|
||||
|
||||
if (replyingMime.Cc.Any() || replyingMime.Bcc.Any())
|
||||
IsCCBCCVisible = true;
|
||||
|
||||
Subject = replyingMime.Subject;
|
||||
|
||||
CurrentMimeMessage = replyingMime;
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
list.Clear();
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace Wino.Mail.ViewModels.Data
|
||||
|
||||
public int HoldingAccountCount => 1;
|
||||
|
||||
public bool HasProfilePicture => !string.IsNullOrEmpty(Account.Base64ProfilePictureData);
|
||||
|
||||
public AccountProviderDetailViewModel(IProviderDetail providerDetail, MailAccount account)
|
||||
{
|
||||
ProviderDetail = providerDetail;
|
||||
|
||||
@@ -3,31 +3,21 @@ using Wino.Messaging.Client.Navigation;
|
||||
|
||||
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 BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive)
|
||||
{
|
||||
Request = request;
|
||||
Title = request.PageTitle;
|
||||
|
||||
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);
|
||||
IsActive = isActive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@ using Wino.Core.Extensions;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data
|
||||
{
|
||||
public class MailAttachmentViewModel : ObservableObject
|
||||
public partial class MailAttachmentViewModel : ObservableObject
|
||||
{
|
||||
private bool isBusy;
|
||||
private readonly MimePart _mimePart;
|
||||
|
||||
public MailAttachmentType AttachmentType { get; }
|
||||
@@ -22,23 +21,21 @@ namespace Wino.Mail.ViewModels.Data
|
||||
/// <summary>
|
||||
/// Gets or sets whether attachment is busy with opening or saving etc.
|
||||
/// </summary>
|
||||
public bool IsBusy
|
||||
{
|
||||
get => isBusy;
|
||||
set => SetProperty(ref isBusy, value);
|
||||
}
|
||||
[ObservableProperty]
|
||||
private bool isBusy;
|
||||
|
||||
public MailAttachmentViewModel(MimePart mimePart)
|
||||
{
|
||||
_mimePart = mimePart;
|
||||
|
||||
var array = new byte[_mimePart.Content.Stream.Length];
|
||||
_mimePart.Content.Stream.Read(array, 0, (int)_mimePart.Content.Stream.Length);
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
Content = array;
|
||||
using (memoryStream) mimePart.Content.DecodeTo(memoryStream);
|
||||
|
||||
Content = memoryStream.ToArray();
|
||||
|
||||
FileName = mimePart.FileName;
|
||||
ReadableSize = mimePart.Content.Stream.Length.GetBytesReadable();
|
||||
ReadableSize = ((long)Content.Length).GetBytesReadable();
|
||||
|
||||
var extension = Path.GetExtension(FileName);
|
||||
AttachmentType = GetAttachmentType(extension);
|
||||
|
||||
@@ -18,8 +18,6 @@ namespace Wino.Mail.ViewModels.Data
|
||||
public string MessageId => ((IMailItem)MailCopy).MessageId;
|
||||
public string FromName => ((IMailItem)MailCopy).FromName ?? FromAddress;
|
||||
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 InReplyTo => ((IMailItem)MailCopy).InReplyTo;
|
||||
|
||||
@@ -77,6 +75,18 @@ namespace Wino.Mail.ViewModels.Data
|
||||
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 MailAccount AssignedAccount => ((IMailItem)MailCopy).AssignedAccount;
|
||||
@@ -94,6 +104,8 @@ namespace Wino.Mail.ViewModels.Data
|
||||
OnPropertyChanged(nameof(DraftId));
|
||||
OnPropertyChanged(nameof(Subject));
|
||||
OnPropertyChanged(nameof(PreviewText));
|
||||
OnPropertyChanged(nameof(FromAddress));
|
||||
OnPropertyChanged(nameof(HasAttachments));
|
||||
}
|
||||
|
||||
public IEnumerable<Guid> GetContainingIds() => new[] { UniqueId };
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace Wino.Mail.ViewModels
|
||||
private CancellationTokenSource listManipulationCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
public IWinoNavigationService NavigationService { get; }
|
||||
public IStatePersistanceService StatePersistanceService { get; }
|
||||
public IStatePersistanceService StatePersistenceService { get; }
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
|
||||
private readonly IMailService _mailService;
|
||||
@@ -120,6 +120,12 @@ namespace Wino.Mail.ViewModels
|
||||
[ObservableProperty]
|
||||
private string barMessage;
|
||||
|
||||
[ObservableProperty]
|
||||
private double mailListLength = 420;
|
||||
|
||||
[ObservableProperty]
|
||||
private double maxMailListLength = 1200;
|
||||
|
||||
[ObservableProperty]
|
||||
private string barTitle;
|
||||
|
||||
@@ -141,7 +147,7 @@ namespace Wino.Mail.ViewModels
|
||||
public MailListPageViewModel(IDialogService dialogService,
|
||||
IWinoNavigationService navigationService,
|
||||
IMailService mailService,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
IStatePersistanceService statePersistenceService,
|
||||
IFolderService folderService,
|
||||
IThreadingStrategyProvider threadingStrategyProvider,
|
||||
IContextMenuItemService contextMenuItemService,
|
||||
@@ -152,7 +158,7 @@ namespace Wino.Mail.ViewModels
|
||||
{
|
||||
PreferencesService = preferencesService;
|
||||
_winoServerConnectionManager = winoServerConnectionManager;
|
||||
StatePersistanceService = statePersistanceService;
|
||||
StatePersistenceService = statePersistenceService;
|
||||
NavigationService = navigationService;
|
||||
|
||||
_mailService = mailService;
|
||||
@@ -165,6 +171,8 @@ namespace Wino.Mail.ViewModels
|
||||
SelectedFilterOption = FilterOptions[0];
|
||||
SelectedSortingOption = SortingOptions[0];
|
||||
|
||||
mailListLength = statePersistenceService.MailListPaneLength;
|
||||
|
||||
selectionChangedObservable = Observable.FromEventPattern<NotifyCollectionChangedEventArgs>(SelectedItems, nameof(SelectedItems.CollectionChanged));
|
||||
selectionChangedObservable
|
||||
.Throttle(TimeSpan.FromMilliseconds(100))
|
||||
@@ -257,7 +265,7 @@ namespace Wino.Mail.ViewModels
|
||||
{
|
||||
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.
|
||||
// This is not the same behavior in Windows Mail,
|
||||
// but it's a trash behavior.
|
||||
@@ -266,7 +274,7 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
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.
|
||||
Messenger.Send(new ShellStateUpdated());
|
||||
|
||||
@@ -273,7 +273,7 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -25,37 +25,24 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
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.Dark, Translator.ElementTheme_Dark),
|
||||
new ElementThemeContainer(ApplicationElementTheme.Default, Translator.ElementTheme_Default),
|
||||
};
|
||||
];
|
||||
|
||||
public List<MailListPaneLengthPreferences> PaneLengths { get; set; } = new List<MailListPaneLengthPreferences>()
|
||||
{
|
||||
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>()
|
||||
{
|
||||
public List<MailListDisplayMode> InformationDisplayModes { get; set; } =
|
||||
[
|
||||
MailListDisplayMode.Compact,
|
||||
MailListDisplayMode.Medium,
|
||||
MailListDisplayMode.Spacious
|
||||
};
|
||||
];
|
||||
|
||||
public List<AppThemeBase> AppThemes { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private MailListPaneLengthPreferences selectedMailListPaneLength;
|
||||
|
||||
[ObservableProperty]
|
||||
private ElementThemeContainer selectedElementTheme;
|
||||
|
||||
@@ -123,6 +110,13 @@ namespace Wino.Mail.ViewModels
|
||||
|
||||
#endregion
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetMailListPaneLength()
|
||||
{
|
||||
StatePersistanceService.MailListPaneLength = 420;
|
||||
DialogService.InfoBarMessage(Translator.GeneralTitle_Info, Translator.Info_MailListSizeResetSuccessMessage, InfoBarMessageType.Success);
|
||||
}
|
||||
|
||||
public AsyncRelayCommand CreateCustomThemeCommand { get; set; }
|
||||
public PersonalizationPageViewModel(IDialogService dialogService,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
@@ -179,7 +173,6 @@ namespace Wino.Mail.ViewModels
|
||||
{
|
||||
SelectedElementTheme = ElementThemes.Find(a => a.NativeTheme == _themeService.RootTheme);
|
||||
SelectedInfoDisplayMode = PreferencesService.MailItemDisplayMode;
|
||||
SelectedMailListPaneLength = PaneLengths.Find(a => a.Length == StatePersistanceService.MailListPaneLength);
|
||||
|
||||
var currentAccentColor = _themeService.AccentColor;
|
||||
|
||||
@@ -289,8 +282,6 @@ namespace Wino.Mail.ViewModels
|
||||
{
|
||||
_themeService.CurrentApplicationThemeId = SelectedAppTheme.Id;
|
||||
}
|
||||
else if (e.PropertyName == nameof(SelectedMailListPaneLength) && SelectedMailListPaneLength != null)
|
||||
StatePersistanceService.MailListPaneLength = SelectedMailListPaneLength.Length;
|
||||
else
|
||||
{
|
||||
if (e.PropertyName == nameof(SelectedInfoDisplayMode))
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EmailValidation" Version="1.2.0" />
|
||||
<PackageReference Include="IsExternalInit" Version="1.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<Application x:Class="Wino.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Wino.Controls"
|
||||
xmlns:selectors="using:Wino.Selectors"
|
||||
xmlns:wino="using:Wino">
|
||||
<Application
|
||||
x:Class="Wino.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Wino.Controls"
|
||||
xmlns:selectors="using:Wino.Selectors"
|
||||
xmlns:wino="using:Wino">
|
||||
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
@@ -48,6 +49,14 @@
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
</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. -->
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="ChildrenTransitions">
|
||||
@@ -60,9 +69,10 @@
|
||||
</Style>
|
||||
|
||||
<!-- Default Style for ContentDialog -->
|
||||
<Style x:Key="WinoDialogStyle"
|
||||
BasedOn="{StaticResource DefaultContentDialogStyle}"
|
||||
TargetType="ContentDialog" />
|
||||
<Style
|
||||
x:Key="WinoDialogStyle"
|
||||
BasedOn="{StaticResource DefaultContentDialogStyle}"
|
||||
TargetType="ContentDialog" />
|
||||
|
||||
<!-- Settings Menu Item Template -->
|
||||
<Style TargetType="controls:SettingsMenuItemControl">
|
||||
@@ -74,63 +84,71 @@
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:SettingsMenuItemControl">
|
||||
<Grid>
|
||||
<Button Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Command="{TemplateBinding Command}"
|
||||
CommandParameter="{TemplateBinding CommandParameter}"
|
||||
IsEnabled="{TemplateBinding IsEnabled}"
|
||||
IsHitTestVisible="{TemplateBinding IsClickable}">
|
||||
<Grid Height="70"
|
||||
Padding="0,6,12,6"
|
||||
CornerRadius="4">
|
||||
<Button
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Command="{TemplateBinding Command}"
|
||||
CommandParameter="{TemplateBinding CommandParameter}"
|
||||
IsEnabled="{TemplateBinding IsEnabled}"
|
||||
IsHitTestVisible="{TemplateBinding IsClickable}">
|
||||
<Grid
|
||||
Height="70"
|
||||
Padding="0,6,12,6"
|
||||
CornerRadius="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="50" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentControl HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Icon}" />
|
||||
<ContentControl
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Icon}" />
|
||||
|
||||
<Grid Grid.Column="1"
|
||||
Margin="4,0"
|
||||
VerticalAlignment="Center"
|
||||
RowSpacing="3">
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
Margin="4,0"
|
||||
VerticalAlignment="Center"
|
||||
RowSpacing="3">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{TemplateBinding Title}" />
|
||||
<TextBlock Grid.Row="1"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{TemplateBinding Description}" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{TemplateBinding Title}" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{TemplateBinding Description}" />
|
||||
</Grid>
|
||||
|
||||
<Viewbox Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Width="16"
|
||||
Height="16"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsNavigateIconVisible}">
|
||||
<Viewbox
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Width="16"
|
||||
Height="16"
|
||||
HorizontalAlignment="Right"
|
||||
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 " />
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<ContentControl Grid.RowSpan="2"
|
||||
Margin="0,0,16,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding SideContent}"
|
||||
IsHitTestVisible="True"
|
||||
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsNavigateIconVisible, Converter={StaticResource ReverseBooleanToVisibilityConverter}}" />
|
||||
<ContentControl
|
||||
Grid.RowSpan="2"
|
||||
Margin="0,0,16,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding SideContent}"
|
||||
IsHitTestVisible="True"
|
||||
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsNavigateIconVisible, Converter={StaticResource ReverseBooleanToVisibilityConverter}}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
@@ -188,16 +206,17 @@
|
||||
<Image Source="/Assets/FileTypes/type_other.png" />
|
||||
</DataTemplate>
|
||||
|
||||
<selectors:FileAttachmentTypeSelector x:Key="FileTypeIconSelector"
|
||||
Archive="{StaticResource ArchiveTemplate}"
|
||||
Executable="{StaticResource ExecutableTemplate}"
|
||||
HTML="{StaticResource HTMLTemplate}"
|
||||
Image="{StaticResource ImageTemplate}"
|
||||
None="{StaticResource NoneTemplate}"
|
||||
Other="{StaticResource OtherTemplate}"
|
||||
PDF="{StaticResource PDFTemplate}"
|
||||
RarArchive="{StaticResource RarTemplate}"
|
||||
Video="{StaticResource VideoTemplate}" />
|
||||
<selectors:FileAttachmentTypeSelector
|
||||
x:Key="FileTypeIconSelector"
|
||||
Archive="{StaticResource ArchiveTemplate}"
|
||||
Executable="{StaticResource ExecutableTemplate}"
|
||||
HTML="{StaticResource HTMLTemplate}"
|
||||
Image="{StaticResource ImageTemplate}"
|
||||
None="{StaticResource NoneTemplate}"
|
||||
Other="{StaticResource OtherTemplate}"
|
||||
PDF="{StaticResource PDFTemplate}"
|
||||
RarArchive="{StaticResource RarTemplate}"
|
||||
Video="{StaticResource VideoTemplate}" />
|
||||
</ResourceDictionary>
|
||||
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
|
||||
@@ -179,6 +179,7 @@ namespace Wino
|
||||
services.AddTransient(typeof(MergedAccountDetailsPageViewModel));
|
||||
services.AddTransient(typeof(LanguageTimePageViewModel));
|
||||
services.AddTransient(typeof(AppPreferencesPageViewModel));
|
||||
services.AddTransient(typeof(AliasManagementPageViewModel));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -208,7 +208,6 @@ namespace Wino.Views
|
||||
WeakReferenceMessenger.Default.Send(new ClearMailSelectionsRequested());
|
||||
WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested());
|
||||
WeakReferenceMessenger.Default.Send(new ShellStateUpdated());
|
||||
|
||||
}
|
||||
|
||||
private async void MenuItemContextRequested(UIElement sender, ContextRequestedEventArgs args)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:xaml="using:Windows.UI.Xaml">
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:xaml="using:Windows.UI.Xaml">
|
||||
|
||||
<x:String x:Key="ThemeName">Mica</x:String>
|
||||
<x:Boolean x:Key="UseMica">True</x:Boolean>
|
||||
|
||||
27
Wino.Mail/Converters/GridLengthConverter.cs
Normal file
27
Wino.Mail/Converters/GridLengthConverter.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Windows.UI.Xaml.Data;
|
||||
using Windows.UI.Xaml;
|
||||
|
||||
namespace Wino.Converters
|
||||
{
|
||||
public class GridLengthConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is double doubleValue)
|
||||
{
|
||||
return new GridLength(doubleValue);
|
||||
}
|
||||
return new GridLength(1, GridUnitType.Auto);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is GridLength gridLength)
|
||||
{
|
||||
return gridLength.Value;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,13 @@
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
|
||||
<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.Setters>
|
||||
<Setter Target="StatusText.Text" Value="{x:Bind domain:Translator.AccountCreationDialog_Completed}" />
|
||||
|
||||
40
Wino.Mail/Dialogs/CreateAccountAliasDialog.xaml
Normal file
40
Wino.Mail/Dialogs/CreateAccountAliasDialog.xaml
Normal file
@@ -0,0 +1,40 @@
|
||||
<ContentDialog
|
||||
x:Class="Wino.Dialogs.CreateAccountAliasDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Wino.Dialogs"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
PrimaryButtonText="{x:Bind domain:Translator.Buttons_Create}"
|
||||
SecondaryButtonText="{x:Bind domain:Translator.Buttons_Cancel}"
|
||||
DefaultButton="Primary"
|
||||
PrimaryButtonClick="CreateClicked"
|
||||
Title="{x:Bind domain:Translator.CreateAccountAliasDialog_Title}"
|
||||
Style="{StaticResource WinoDialogStyle}">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{x:Bind domain:Translator.CreateAccountAliasDialog_Description}" Style="{StaticResource CaptionTextBlockStyle}" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0,20"
|
||||
Spacing="8">
|
||||
<TextBox
|
||||
x:Name="AliasTextBox"
|
||||
PlaceholderText="{x:Bind domain:Translator.CreateAccountAliasDialog_AliasAddressPlaceholder}"
|
||||
Header="{x:Bind domain:Translator.CreateAccountAliasDialog_AliasAddress}" />
|
||||
|
||||
<TextBox
|
||||
x:Name="ReplyToTextBox"
|
||||
PlaceholderText="{x:Bind domain:Translator.CreateAccountAliasDialog_ReplyToAddressPlaceholder}"
|
||||
Header="{x:Bind domain:Translator.CreateAccountAliasDialog_ReplyToAddress}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
|
||||
30
Wino.Mail/Dialogs/CreateAccountAliasDialog.xaml.cs
Normal file
30
Wino.Mail/Dialogs/CreateAccountAliasDialog.xaml.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Dialogs
|
||||
{
|
||||
public sealed partial class CreateAccountAliasDialog : ContentDialog, ICreateAccountAliasDialog
|
||||
{
|
||||
public MailAccountAlias CreatedAccountAlias { get; set; }
|
||||
public CreateAccountAliasDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void CreateClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
CreatedAccountAlias = new MailAccountAlias
|
||||
{
|
||||
AliasAddress = AliasTextBox.Text.Trim(),
|
||||
ReplyToAddress = ReplyToTextBox.Text.Trim(),
|
||||
Id = Guid.NewGuid(),
|
||||
IsPrimary = false,
|
||||
IsVerified = false
|
||||
};
|
||||
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,14 +56,6 @@
|
||||
PlaceholderText="{x:Bind domain:Translator.NewAccountDialog_AccountNamePlaceholder}"
|
||||
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.
|
||||
Provider selection should be first, then account details.
|
||||
@@ -79,7 +71,7 @@
|
||||
|
||||
<ListView
|
||||
Grid.Row="2"
|
||||
Margin="0,12"
|
||||
|
||||
Padding="0"
|
||||
ItemTemplate="{StaticResource NewMailProviderTemplate}"
|
||||
ItemsSource="{x:Bind Providers}"
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace Wino.Dialogs
|
||||
|
||||
if (IsSecondaryButtonEnabled)
|
||||
{
|
||||
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim(), SenderNameTextbox.Text.Trim());
|
||||
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim());
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
@@ -68,8 +68,7 @@ namespace Wino.Dialogs
|
||||
{
|
||||
bool shouldEnable = SelectedMailProvider != null
|
||||
&& SelectedMailProvider.IsSupported
|
||||
&& !string.IsNullOrEmpty(AccountNameTextbox.Text)
|
||||
&& (SelectedMailProvider.RequireSenderNameOnCreationDialog ? !string.IsNullOrEmpty(SenderNameTextbox.Text) : true);
|
||||
&& !string.IsNullOrEmpty(AccountNameTextbox.Text);
|
||||
|
||||
IsPrimaryButtonEnabled = shouldEnable;
|
||||
}
|
||||
@@ -77,7 +76,6 @@ namespace Wino.Dialogs
|
||||
private void ValidateNames()
|
||||
{
|
||||
AccountNameTextbox.IsEnabled = SelectedMailProvider != null;
|
||||
SenderNameTextbox.IsEnabled = SelectedMailProvider != null && SelectedMailProvider.Type != Core.Domain.Enums.MailProviderType.IMAP4;
|
||||
}
|
||||
|
||||
private void DialogOpened(ContentDialog sender, ContentDialogOpenedEventArgs args) => Validate();
|
||||
|
||||
@@ -216,6 +216,18 @@ namespace Wino.Services
|
||||
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)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -11,6 +11,7 @@ using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Helpers;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Messages;
|
||||
using Wino.Messaging.Client.Mails;
|
||||
using Wino.Views;
|
||||
using Wino.Views.Account;
|
||||
using Wino.Views.Settings;
|
||||
@@ -88,6 +89,8 @@ namespace Wino.Services
|
||||
return typeof(SettingOptionsPage);
|
||||
case WinoPage.AppPreferencesPage:
|
||||
return typeof(AppPreferencesPage);
|
||||
case WinoPage.AliasManagementPage:
|
||||
return typeof(AliasManagementPage);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -114,6 +117,7 @@ namespace Wino.Services
|
||||
{
|
||||
// No need for new navigation, just refresh the folder.
|
||||
WeakReferenceMessenger.Default.Send(new ActiveMailFolderChangedEvent(folderNavigationArgs.BaseFolderMenuItem, folderNavigationArgs.FolderInitLoadAwaitTask));
|
||||
WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested());
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -136,6 +140,13 @@ namespace Wino.Services
|
||||
{
|
||||
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
|
||||
{
|
||||
listingFrame.Navigate(pageType, parameter, transitionInfo);
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
<SolidColorBrush x:Key="FolderSyncBrush">#1abc9c</SolidColorBrush>
|
||||
|
||||
<SolidColorBrush x:Key="AliasUnverifiedBrush">#ff7675</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="AliasVerifiedBrush">#1abc9c</SolidColorBrush>
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Name="Light">
|
||||
<SolidColorBrush x:Key="AttachmentBrush">#fdcb6e</SolidColorBrush>
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
xmlns:converters="using:Wino.Converters">
|
||||
<converters:ReverseBooleanToVisibilityConverter x:Key="ReverseBooleanToVisibilityConverter" />
|
||||
<converters:ReverseBooleanConverter x:Key="ReverseBooleanConverter" />
|
||||
<converters:GridLengthConverter x:Key="GridLengthConverter" />
|
||||
</ResourceDictionary>
|
||||
|
||||
6
Wino.Mail/Views/Abstract/AliasManagementPageAbstract.cs
Normal file
6
Wino.Mail/Views/Abstract/AliasManagementPageAbstract.cs
Normal 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
@@ -443,17 +443,9 @@
|
||||
x:Name="AccountsComboBox"
|
||||
Grid.Column="1"
|
||||
IsEditable="False"
|
||||
IsEnabled="False"
|
||||
ItemsSource="{x:Bind ViewModel.Accounts}"
|
||||
SelectedItem="{x:Bind ViewModel.ComposingAccount, 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>
|
||||
ItemsSource="{x:Bind ViewModel.AvailableAliases, Mode=OneWay}"
|
||||
DisplayMemberPath="AliasAddress"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedAlias, Mode=TwoWay}" />
|
||||
|
||||
<!-- To -->
|
||||
<TextBlock
|
||||
|
||||
@@ -39,7 +39,8 @@ namespace Wino.Views
|
||||
public sealed partial class ComposePage : ComposePageAbstract,
|
||||
IRecipient<NavigationPaneModeChanged>,
|
||||
IRecipient<CreateNewComposeMailRequested>,
|
||||
IRecipient<ApplicationThemeChanged>
|
||||
IRecipient<ApplicationThemeChanged>,
|
||||
IRecipient<KillChromiumRequested>
|
||||
{
|
||||
public bool IsComposerDarkMode
|
||||
{
|
||||
@@ -237,12 +238,9 @@ namespace Wino.Views
|
||||
// Convert files to MailAttachmentViewModel.
|
||||
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);
|
||||
}
|
||||
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||
{
|
||||
base.OnNavigatingFrom(e);
|
||||
|
||||
DisposeDisposables();
|
||||
DisposeWebView2();
|
||||
}
|
||||
|
||||
private void DisposeWebView2()
|
||||
{
|
||||
@@ -700,5 +691,11 @@ namespace Wino.Views
|
||||
ToBox.Focus(FocusState.Programmatic);
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(KillChromiumRequested message)
|
||||
{
|
||||
DisposeDisposables();
|
||||
DisposeWebView2();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
xmlns:collections="using:CommunityToolkit.Mvvm.Collections"
|
||||
xmlns:controls="using:Wino.Controls"
|
||||
xmlns:controls1="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:converters="using:Wino.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
xmlns:enums="using:Wino.Core.Domain.Enums"
|
||||
@@ -20,9 +21,8 @@
|
||||
xmlns:ui="using:Microsoft.Toolkit.Uwp.UI"
|
||||
xmlns:viewModelData="using:Wino.Mail.ViewModels.Data"
|
||||
xmlns:wino="using:Wino"
|
||||
xmlns:converters="using:Wino.Converters"
|
||||
x:Name="root"
|
||||
Loaded="MailListPageLoaded"
|
||||
SizeChanged="PageSizeChanged"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
@@ -181,11 +181,11 @@
|
||||
<DataTemplate x:DataType="viewModelData:ThreadMailItemViewModel">
|
||||
<controls:WinoExpander
|
||||
x:Name="ThreadExpander"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
BackgroundSizing="InnerBorderEdge"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
IsExpanded="{x:Bind IsThreadExpanded, Mode=TwoWay}">
|
||||
<muxc:Expander.Header>
|
||||
<controls:MailItemDisplayInformationControl
|
||||
@@ -276,9 +276,9 @@
|
||||
|
||||
<AutoSuggestBox
|
||||
x:Name="SearchBar"
|
||||
Margin="2,0,-2,0"
|
||||
VerticalAlignment="Center"
|
||||
BorderBrush="Transparent"
|
||||
Margin="2,0,-2,0"
|
||||
GotFocus="SearchBoxFocused"
|
||||
LostFocus="SearchBarUnfocused"
|
||||
PlaceholderText="{x:Bind domain:Translator.SearchBarPlaceholder}"
|
||||
@@ -296,23 +296,20 @@
|
||||
|
||||
<Grid x:Name="RootGrid" Padding="0,0,0,7">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition
|
||||
x:Name="ReaderColumn"
|
||||
Width="*"
|
||||
MaxWidth="{x:Bind ViewModel.StatePersistanceService.MailListPaneLength, Mode=OneWay}" />
|
||||
<ColumnDefinition x:Name="MailListColumn" Width="{x:Bind ViewModel.MailListLength, Mode=OneWay, Converter={StaticResource GridLengthConverter}}" />
|
||||
<ColumnDefinition x:Name="RendererColumn" Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Mail Items -->
|
||||
<Border
|
||||
x:Name="ReaderGridContainer"
|
||||
x:Name="MailListContainer"
|
||||
Grid.Column="0"
|
||||
Padding="5,0,0,0"
|
||||
Background="{ThemeResource WinoContentZoneBackgroud}"
|
||||
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="7">
|
||||
<Grid x:Name="ReaderGrid">
|
||||
<Grid x:Name="MailListGrid">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -335,11 +332,6 @@
|
||||
Padding="2,0"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
CornerRadius="8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="48" />
|
||||
<RowDefinition Height="2" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Commands -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -481,12 +473,6 @@
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<muxc:ProgressBar
|
||||
x:Name="LoadingProgressBar"
|
||||
Grid.Row="1"
|
||||
x:Load="{x:Bind ViewModel.IsInitializingFolder, Mode=OneWay}"
|
||||
IsIndeterminate="True" />
|
||||
</Grid>
|
||||
|
||||
<!-- Pivot + Sync + Multi Select -->
|
||||
@@ -615,11 +601,11 @@
|
||||
<SemanticZoom x:Name="SemanticZoomContainer" CanChangeViews="{x:Bind ViewModel.PreferencesService.IsSemanticZoomEnabled, Mode=OneWay}">
|
||||
<SemanticZoom.ZoomedInView>
|
||||
<listview:WinoListView
|
||||
ui:ScrollViewerExtensions.VerticalScrollBarMargin="0"
|
||||
x:Name="MailListView"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ui:ScrollViewerExtensions.EnableMiddleClickScrolling="True"
|
||||
ui:ListViewExtensions.ItemContainerStretchDirection="Horizontal"
|
||||
ui:ScrollViewerExtensions.EnableMiddleClickScrolling="True"
|
||||
ui:ScrollViewerExtensions.VerticalScrollBarMargin="0"
|
||||
ItemDeletedCommand="{x:Bind ViewModel.MailOperationCommand}"
|
||||
ItemTemplateSelector="{StaticResource MailItemDisplaySelector}"
|
||||
ItemsSource="{x:Bind MailCollectionViewSource.View, Mode=OneWay}"
|
||||
@@ -695,74 +681,39 @@
|
||||
|
||||
</Grid>
|
||||
</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 -->
|
||||
<Frame x:Name="RenderingFrame" IsNavigationStackEnabled="False" />
|
||||
|
||||
<!-- No Mail Selected Message -->
|
||||
<StackPanel
|
||||
x:Name="NoMailSelectedPanel"
|
||||
Opacity="0.5"
|
||||
Spacing="6"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
VerticalAlignment="Center"
|
||||
Opacity="0.5"
|
||||
Spacing="6">
|
||||
|
||||
<controls:WinoFontIcon Icon="Mail" FontSize="80" />
|
||||
<controls:WinoFontIcon FontSize="80" Icon="Mail" />
|
||||
|
||||
<TextBlock
|
||||
x:Name="CountTextBlock"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="31"
|
||||
Text="{x:Bind ViewModel.SelectedMessageText, Mode=OneWay}"
|
||||
Style="{StaticResource SubheaderTextBlockStyle}"
|
||||
x:Name="CountTextBlock" />
|
||||
Text="{x:Bind ViewModel.SelectedMessageText, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</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>
|
||||
</abstract:MailListPageAbstract>
|
||||
|
||||
@@ -40,11 +40,9 @@ namespace Wino.Views
|
||||
IRecipient<ShellStateUpdated>,
|
||||
IRecipient<DisposeRenderingFrameRequested>
|
||||
{
|
||||
private const string NarrowVisualStateKey = "NarrowState";
|
||||
private const string AdaptivenessStatesKey = "AdaptiveStates";
|
||||
private const double RENDERING_COLUMN_MIN_WIDTH = 300;
|
||||
|
||||
private IStatePersistanceService StatePersistanceService { get; } = App.Current.Services.GetService<IStatePersistanceService>();
|
||||
private IPreferencesService PreferencesService { get; } = App.Current.Services.GetService<IPreferencesService>();
|
||||
private IStatePersistanceService StatePersistenceService { get; } = App.Current.Services.GetService<IStatePersistanceService>();
|
||||
private IKeyPressService KeyPressService { get; } = App.Current.Services.GetService<IKeyPressService>();
|
||||
|
||||
public MailListPage()
|
||||
@@ -90,7 +88,7 @@ namespace Wino.Views
|
||||
SelectAllCheckbox.Unchecked += SelectAllCheckboxUnchecked;
|
||||
}
|
||||
|
||||
private void SelectionModeToggleChecked(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
||||
private void SelectionModeToggleChecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
private void SelectAllCheckboxChecked(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
||||
private void SelectAllCheckboxChecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MailListView.SelectAllWino();
|
||||
}
|
||||
|
||||
private void SelectAllCheckboxUnchecked(object sender, Windows.UI.Xaml.RoutedEventArgs e)
|
||||
private void SelectAllCheckboxUnchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
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)
|
||||
{
|
||||
// Single item in thread selected.
|
||||
@@ -446,21 +373,6 @@ namespace Wino.Views
|
||||
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)
|
||||
{
|
||||
if (args.Key == Windows.System.VirtualKey.Delete)
|
||||
@@ -568,5 +480,89 @@ namespace Wino.Views
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace Wino.Views
|
||||
WinoPage.SignatureManagementPage => typeof(SignatureManagementPage),
|
||||
WinoPage.AccountDetailsPage => typeof(AccountDetailsPage),
|
||||
WinoPage.MergedAccountDetailsPage => typeof(MergedAccountDetailsPage),
|
||||
WinoPage.AliasManagementPage => typeof(AliasManagementPage),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
164
Wino.Mail/Views/Settings/AliasManagementPage.xaml
Normal file
164
Wino.Mail/Views/Settings/AliasManagementPage.xaml
Normal file
File diff suppressed because one or more lines are too long
12
Wino.Mail/Views/Settings/AliasManagementPage.xaml.cs
Normal file
12
Wino.Mail/Views/Settings/AliasManagementPage.xaml.cs
Normal 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
@@ -169,6 +169,9 @@
|
||||
<PackageReference Include="CommunityToolkit.Uwp.Animations">
|
||||
<Version>8.0.240109</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Uwp.Behaviors">
|
||||
<Version>8.0.240109</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Uwp.Controls.SettingsControls">
|
||||
<Version>8.0.240109</Version>
|
||||
</PackageReference>
|
||||
@@ -246,6 +249,7 @@
|
||||
<Compile Include="Controls\WinoFontIconSource.cs" />
|
||||
<Compile Include="Controls\WinoFontIcon.cs" />
|
||||
<Compile Include="Controls\WinoSwipeControlItems.cs" />
|
||||
<Compile Include="Converters\GridLengthConverter.cs" />
|
||||
<Compile Include="Dialogs\AccountEditDialog.xaml.cs">
|
||||
<DependentUpon>AccountEditDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@@ -268,6 +272,9 @@
|
||||
<Compile Include="Dialogs\NewImapSetupDialog.xaml.cs">
|
||||
<DependentUpon>NewImapSetupDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Dialogs\CreateAccountAliasDialog.xaml.cs">
|
||||
<DependentUpon>CreateAccountAliasDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Dialogs\StoreRatingDialog.xaml.cs">
|
||||
<DependentUpon>StoreRatingDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@@ -340,6 +347,7 @@
|
||||
<Compile Include="Views\Abstract\AboutPageAbstract.cs" />
|
||||
<Compile Include="Views\Abstract\AccountDetailsPageAbstract.cs" />
|
||||
<Compile Include="Views\Abstract\AccountManagementPageAbstract.cs" />
|
||||
<Compile Include="Views\Abstract\AliasManagementPageAbstract.cs" />
|
||||
<Compile Include="Views\Abstract\AppPreferencesPageAbstract.cs" />
|
||||
<Compile Include="Views\Abstract\AppShellAbstract.cs" />
|
||||
<Compile Include="Views\Abstract\ComposePageAbstract.cs" />
|
||||
@@ -423,6 +431,9 @@
|
||||
<Compile Include="Views\Settings\SettingOptionsPage.xaml.cs">
|
||||
<DependentUpon>SettingOptionsPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\Settings\AliasManagementPage.xaml.cs">
|
||||
<DependentUpon>AliasManagementPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\Settings\SignatureManagementPage.xaml.cs">
|
||||
<DependentUpon>SignatureManagementPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@@ -509,6 +520,10 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Dialogs\CreateAccountAliasDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Dialogs\StoreRatingDialog.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -657,6 +672,10 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\Settings\AliasManagementPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Views\Settings\SignatureManagementPage.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
7
Wino.Messages/Client/Mails/KillChromiumRequested.cs
Normal file
7
Wino.Messages/Client/Mails/KillChromiumRequested.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Wino.Messaging.Client.Mails
|
||||
{
|
||||
/// <summary>
|
||||
/// Terminates all chromum instances.
|
||||
/// </summary>
|
||||
public record KillChromiumRequested;
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
<Identity
|
||||
Name="58272BurakKSE.WinoMailPreview"
|
||||
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
|
||||
Version="1.8.1.0" />
|
||||
Version="1.8.2.0" />
|
||||
|
||||
<Extensions>
|
||||
<!-- Publisher Cache Folders -->
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace Wino.Server.MessageHandlers
|
||||
{
|
||||
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);
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||
<PackageReference Include="H.NotifyIcon.Wpf" Version="2.1.0" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.0.3" />
|
||||
<PackageReference Include="H.NotifyIcon.Wpf" Version="2.1.2" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user