diff --git a/Wino.Core.Domain/Entities/MailAccount.cs b/Wino.Core.Domain/Entities/MailAccount.cs index 60568d32..c1bc43a1 100644 --- a/Wino.Core.Domain/Entities/MailAccount.cs +++ b/Wino.Core.Domain/Entities/MailAccount.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using SQLite; using Wino.Core.Domain.Enums; @@ -48,7 +47,7 @@ namespace Wino.Core.Domain.Entities /// /// Base64 encoded profile picture of the account. /// - public string ProfilePictureBase64 { get; set; } + public string Base64ProfilePictureData { get; set; } /// /// Gets or sets the listing order of the account in the accounts list. @@ -84,8 +83,8 @@ namespace Wino.Core.Domain.Entities /// It's only synchronized for Gmail right now. /// Other provider types are manually added by users and not verified. /// - [Ignore] - public List Aliases { get; set; } + //[Ignore] + //public List Aliases { get; set; } /// /// Account preferences. diff --git a/Wino.Core.Domain/Entities/MailAccountAlias.cs b/Wino.Core.Domain/Entities/MailAccountAlias.cs index f1f0b623..8d103744 100644 --- a/Wino.Core.Domain/Entities/MailAccountAlias.cs +++ b/Wino.Core.Domain/Entities/MailAccountAlias.cs @@ -32,6 +32,13 @@ namespace Wino.Core.Domain.Entities /// public bool IsPrimary { get; set; } + /// + /// 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. + /// + public bool IsRootAlias { get; set; } + /// /// Whether the alias is verified by the server. /// Non-verified aliases will show an info tip to users during sending. @@ -40,6 +47,11 @@ namespace Wino.Core.Domain.Entities /// public bool IsVerified { get; set; } + /// + /// Root aliases can't be deleted. + /// + public bool CanDelete => !IsRootAlias; + public override bool Equals(object obj) { if (obj == null || GetType() != obj.GetType()) @@ -51,17 +63,20 @@ namespace Wino.Core.Domain.Entities AliasAddress == other.AliasAddress && ReplyToAddress == other.ReplyToAddress && IsPrimary == other.IsPrimary && - IsVerified == other.IsVerified; + IsVerified == other.IsVerified && + IsRootAlias == other.IsRootAlias; } public override int GetHashCode() { - int hashCode = -753829106; + int hashCode = 59052167; hashCode = hashCode * -1521134295 + AccountId.GetHashCode(); hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AliasAddress); hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ReplyToAddress); hashCode = hashCode * -1521134295 + IsPrimary.GetHashCode(); + hashCode = hashCode * -1521134295 + IsRootAlias.GetHashCode(); hashCode = hashCode * -1521134295 + IsVerified.GetHashCode(); + hashCode = hashCode * -1521134295 + CanDelete.GetHashCode(); return hashCode; } } diff --git a/Wino.Core.Domain/Enums/AccountCreationDialogState.cs b/Wino.Core.Domain/Enums/AccountCreationDialogState.cs index af724f47..947449d4 100644 --- a/Wino.Core.Domain/Enums/AccountCreationDialogState.cs +++ b/Wino.Core.Domain/Enums/AccountCreationDialogState.cs @@ -9,6 +9,7 @@ ManuelSetupWaiting, TestingConnection, AutoDiscoverySetup, - AutoDiscoveryInProgress + AutoDiscoveryInProgress, + FetchingProfileInformation } } diff --git a/Wino.Core.Domain/Enums/SynchronizationType.cs b/Wino.Core.Domain/Enums/SynchronizationType.cs index c95b7d52..927016d6 100644 --- a/Wino.Core.Domain/Enums/SynchronizationType.cs +++ b/Wino.Core.Domain/Enums/SynchronizationType.cs @@ -7,5 +7,6 @@ Inbox, // Only Inbox Custom, // Only sync folders that are specified in the options. Full, // Synchronize everything + UpdateProfile, // Only update profile information } } diff --git a/Wino.Core.Domain/Enums/WinoPage.cs b/Wino.Core.Domain/Enums/WinoPage.cs index 2a7264a2..cf3b5292 100644 --- a/Wino.Core.Domain/Enums/WinoPage.cs +++ b/Wino.Core.Domain/Enums/WinoPage.cs @@ -23,5 +23,6 @@ LanguageTimePage, AppPreferencesPage, SettingOptionsPage, + AliasManagementPage } } diff --git a/Wino.Core.Domain/Interfaces/IAccountService.cs b/Wino.Core.Domain/Interfaces/IAccountService.cs index 926a65d6..e7196653 100644 --- a/Wino.Core.Domain/Interfaces/IAccountService.cs +++ b/Wino.Core.Domain/Interfaces/IAccountService.cs @@ -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 { @@ -101,12 +102,42 @@ namespace Wino.Core.Domain.Interfaces /// AccountId-OrderNumber pair for all accounts. Task UpdateAccountOrdersAsync(Dictionary accountIdOrderPair); + /// + /// Returns the account aliases. + /// + /// Account id. + /// A list of MailAccountAlias that has e-mail aliases. + Task> GetAccountAliasesAsync(Guid accountId); + /// /// Updated account's aliases. /// /// Account id to update aliases for. /// Full list of updated aliases. /// - Task UpdateAccountAliases(Guid accountId, List aliases); + Task UpdateAccountAliasesAsync(Guid accountId, List aliases); + + /// + /// Delete account alias. + /// + /// Alias to remove. + Task DeleteAccountAliasAsync(Guid aliasId); + + /// + /// Updated profile information of the account. + /// + /// Account id to update info for. + /// Info data. + /// + Task UpdateProfileInformationAsync(Guid accountId, ProfileInformation profileInformation); + + + /// + /// Creates a root + primary alias for the account. + /// This is only called when the account is created. + /// + /// Account id. + /// Address to create root primary alias from. + Task CreateRootAliasAsync(Guid accountId, string address); } } diff --git a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs index 21ca1f8a..e6ccd2a1 100644 --- a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs +++ b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs @@ -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 /// Result summary of synchronization. Task SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default); + /// + /// Synchronizes profile information with the server. + /// Sender name and + /// + /// + Task SynchronizeProfileInformationAsync(); + /// /// Downloads a single MIME message from the server and saves it to disk. /// diff --git a/Wino.Core.Domain/Interfaces/ICreateAccountAliasDialog.cs b/Wino.Core.Domain/Interfaces/ICreateAccountAliasDialog.cs new file mode 100644 index 00000000..d27a0229 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/ICreateAccountAliasDialog.cs @@ -0,0 +1,9 @@ +using Wino.Core.Domain.Entities; + +namespace Wino.Core.Domain.Interfaces +{ + public interface ICreateAccountAliasDialog + { + public MailAccountAlias CreatedAccountAlias { get; set; } + } +} diff --git a/Wino.Core.Domain/Interfaces/IDialogService.cs b/Wino.Core.Domain/Interfaces/IDialogService.cs index a2986bd9..9d5fad1b 100644 --- a/Wino.Core.Domain/Interfaces/IDialogService.cs +++ b/Wino.Core.Domain/Interfaces/IDialogService.cs @@ -53,5 +53,6 @@ namespace Wino.Core.Domain.Interfaces /// /// Signature information. Null if canceled. Task ShowSignatureEditorDialog(AccountSignature signatureModel = null); + Task ShowCreateAccountAliasDialogAsync(); } } diff --git a/Wino.Core.Domain/Models/Accounts/AccountCreationDialogResult.cs b/Wino.Core.Domain/Models/Accounts/AccountCreationDialogResult.cs index a1081b9e..428d1307 100644 --- a/Wino.Core.Domain/Models/Accounts/AccountCreationDialogResult.cs +++ b/Wino.Core.Domain/Models/Accounts/AccountCreationDialogResult.cs @@ -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 = ""); } diff --git a/Wino.Core.Domain/Models/Accounts/ProfileInformation.cs b/Wino.Core.Domain/Models/Accounts/ProfileInformation.cs new file mode 100644 index 00000000..b9fb5677 --- /dev/null +++ b/Wino.Core.Domain/Models/Accounts/ProfileInformation.cs @@ -0,0 +1,9 @@ +namespace Wino.Core.Domain.Models.Accounts +{ + /// + /// Encapsulates the profile information of an account. + /// + /// Display sender name for the account. + /// Base 64 encoded profile picture data of the account. Thumbnail size. + public record ProfileInformation(string SenderName, string Base64ProfilePictureData); +} diff --git a/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs b/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs index 4f794749..dc9a5824 100644 --- a/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs +++ b/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs @@ -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) { diff --git a/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs b/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs index 19e45eb7..885a4b9b 100644 --- a/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs +++ b/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs @@ -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,18 @@ namespace Wino.Core.Domain.Models.Synchronization /// It's ignored in serialization. Client should not react to this. /// [JsonIgnore] - public IEnumerable DownloadedMessages { get; set; } = new List(); + public IEnumerable 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 downloadedMessages) - => new() { DownloadedMessages = downloadedMessages, CompletedState = SynchronizationCompletedState.Success }; + public static SynchronizationResult Completed(IEnumerable 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 }; } } diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 2f6699c7..b77392dc 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -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", @@ -64,6 +65,14 @@ "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_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,6 +101,12 @@ "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", @@ -113,6 +128,7 @@ "Exception_CustomThemeMissingName": "You must provide a name.", "Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.", "Exception_FailedToSynchronizeFolders": "Failed to synchronize folders", + "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 +269,7 @@ "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", "ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method", "ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security", "ImapAuthenticationMethod_Auto": "Auto", @@ -396,6 +413,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 +425,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.", diff --git a/Wino.Core.Domain/Translator.Designer.cs b/Wino.Core.Domain/Translator.Designer.cs index 6fc65a03..a20c9711 100644 --- a/Wino.Core.Domain/Translator.Designer.cs +++ b/Wino.Core.Domain/Translator.Designer.cs @@ -38,6 +38,11 @@ namespace Wino.Core.Domain /// public static string AccountCreationDialog_SigninIn => Resources.GetTranslatedString(@"AccountCreationDialog_SigninIn"); + /// + /// Fetching profile details. + /// + public static string AccountCreationDialog_FetchingProfileInformation => Resources.GetTranslatedString(@"AccountCreationDialog_FetchingProfileInformation"); + /// /// Account Name /// @@ -343,6 +348,46 @@ namespace Wino.Core.Domain /// public static string DialogMessage_AccountLimitTitle => Resources.GetTranslatedString(@"DialogMessage_AccountLimitTitle"); + /// + /// Existing Alias + /// + public static string DialogMessage_AliasExistsTitle => Resources.GetTranslatedString(@"DialogMessage_AliasExistsTitle"); + + /// + /// This alias is already in use. + /// + public static string DialogMessage_AliasExistsMessage => Resources.GetTranslatedString(@"DialogMessage_AliasExistsMessage"); + + /// + /// Invalid Alias + /// + public static string DialogMessage_InvalidAliasTitle => Resources.GetTranslatedString(@"DialogMessage_InvalidAliasTitle"); + + /// + /// This alias is not valid. Make sure all addresses of the alias are valid e-mail addresses. + /// + public static string DialogMessage_InvalidAliasMessage => Resources.GetTranslatedString(@"DialogMessage_InvalidAliasMessage"); + + /// + /// Can't Delete Alias + /// + public static string DialogMessage_CantDeleteRootAliasTitle => Resources.GetTranslatedString(@"DialogMessage_CantDeleteRootAliasTitle"); + + /// + /// Root alias can't be deleted. This is your main identity associated with your account setup. + /// + public static string DialogMessage_CantDeleteRootAliasMessage => Resources.GetTranslatedString(@"DialogMessage_CantDeleteRootAliasMessage"); + + /// + /// Created New Alias + /// + public static string DialogMessage_AliasCreatedTitle => Resources.GetTranslatedString(@"DialogMessage_AliasCreatedTitle"); + + /// + /// New alias is succesfully created. + /// + public static string DialogMessage_AliasCreatedMessage => Resources.GetTranslatedString(@"DialogMessage_AliasCreatedMessage"); + /// /// Do you want to permanently delete all the mails in this folder? /// @@ -434,7 +479,7 @@ namespace Wino.Core.Domain public static string DialogMessage_UnlinkAccountsConfirmationTitle => Resources.GetTranslatedString(@"DialogMessage_UnlinkAccountsConfirmationTitle"); /// - /// Missin Subject + /// Missing Subject /// public static string DialogMessage_EmptySubjectConfirmation => Resources.GetTranslatedString(@"DialogMessage_EmptySubjectConfirmation"); @@ -483,6 +528,36 @@ namespace Wino.Core.Domain /// public static string Dialog_DontAskAgain => Resources.GetTranslatedString(@"Dialog_DontAskAgain"); + /// + /// Create Account Alias + /// + public static string CreateAccountAliasDialog_Title => Resources.GetTranslatedString(@"CreateAccountAliasDialog_Title"); + + /// + /// Make sure your outgoing server allows sending mails from this alias. + /// + public static string CreateAccountAliasDialog_Description => Resources.GetTranslatedString(@"CreateAccountAliasDialog_Description"); + + /// + /// Address + /// + public static string CreateAccountAliasDialog_AliasAddress => Resources.GetTranslatedString(@"CreateAccountAliasDialog_AliasAddress"); + + /// + /// eg. support@mydomain.com + /// + public static string CreateAccountAliasDialog_AliasAddressPlaceholder => Resources.GetTranslatedString(@"CreateAccountAliasDialog_AliasAddressPlaceholder"); + + /// + /// Reply-To Address + /// + public static string CreateAccountAliasDialog_ReplyToAddress => Resources.GetTranslatedString(@"CreateAccountAliasDialog_ReplyToAddress"); + + /// + /// admin@mydomain.com + /// + public static string CreateAccountAliasDialog_ReplyToAddressPlaceholder => Resources.GetTranslatedString(@"CreateAccountAliasDialog_ReplyToAddressPlaceholder"); + /// /// 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. /// @@ -588,6 +663,11 @@ namespace Wino.Core.Domain /// public static string Exception_FailedToSynchronizeFolders => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeFolders"); + /// + /// Failed to synchronize profile information + /// + public static string Exception_FailedToSynchronizeProfileInformation => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeProfileInformation"); + /// /// Callback uri is null on activation. /// @@ -1288,6 +1368,11 @@ namespace Wino.Core.Domain /// public static string Info_UnsubscribeErrorMessage => Resources.GetTranslatedString(@"Info_UnsubscribeErrorMessage"); + /// + /// Primary alias can't be deleted. Please change your alias before deleting this one + /// + public static string Info_CantDeletePrimaryAliasMessage => Resources.GetTranslatedString(@"Info_CantDeletePrimaryAliasMessage"); + /// /// Authentication method /// @@ -2003,6 +2088,16 @@ namespace Wino.Core.Domain /// public static string SettingsFolderOptions_Description => Resources.GetTranslatedString(@"SettingsFolderOptions_Description"); + /// + /// Aliases + /// + public static string SettingsManageAliases_Title => Resources.GetTranslatedString(@"SettingsManageAliases_Title"); + + /// + /// See e-mail aliases assigned for this account, update or delete them. + /// + public static string SettingsManageAliases_Description => Resources.GetTranslatedString(@"SettingsManageAliases_Description"); + /// /// Center Action /// @@ -2053,6 +2148,31 @@ namespace Wino.Core.Domain /// public static string CategoriesFolderNameOverride => Resources.GetTranslatedString(@"CategoriesFolderNameOverride"); + /// + /// Verified + /// + public static string AccountAlias_Column_Verified => Resources.GetTranslatedString(@"AccountAlias_Column_Verified"); + + /// + /// Alias + /// + public static string AccountAlias_Column_Alias => Resources.GetTranslatedString(@"AccountAlias_Column_Alias"); + + /// + /// Primary + /// + public static string AccountAlias_Column_IsPrimaryAlias => Resources.GetTranslatedString(@"AccountAlias_Column_IsPrimaryAlias"); + + /// + /// Wino can only import aliases for your Gmail accounts. + /// + public static string AccountAlias_Disclaimer_FirstLine => Resources.GetTranslatedString(@"AccountAlias_Disclaimer_FirstLine"); + + /// + /// If you want to use aliases for your Outlook or IMAP account, please add them yourself. + /// + public static string AccountAlias_Disclaimer_SecondLine => Resources.GetTranslatedString(@"AccountAlias_Disclaimer_SecondLine"); + /// /// More /// diff --git a/Wino.Core/Extensions/GoogleIntegratorExtensions.cs b/Wino.Core/Extensions/GoogleIntegratorExtensions.cs index 263669ab..ed290d13 100644 --- a/Wino.Core/Extensions/GoogleIntegratorExtensions.cs +++ b/Wino.Core/Extensions/GoogleIntegratorExtensions.cs @@ -205,21 +205,22 @@ namespace Wino.Core.Extensions }; } - public static List GetMailAliases(this ListSendAsResponse response, MailAccount currentAccount) + public static List GetMailAliases(this ListSendAsResponse response, List currentAliases, MailAccount account) { - if (response == null || response.SendAs == null) return currentAccount.Aliases; + if (response == null || response.SendAs == null) return currentAliases; var remoteAliases = response.SendAs.Select(a => new MailAccountAlias() { - AccountId = currentAccount.Id, + AccountId = account.Id, AliasAddress = a.SendAsEmail, IsPrimary = a.IsPrimary.GetValueOrDefault(), - ReplyToAddress = string.IsNullOrEmpty(a.ReplyToAddress) ? currentAccount.Address : a.ReplyToAddress, + ReplyToAddress = string.IsNullOrEmpty(a.ReplyToAddress) ? account.Address : a.ReplyToAddress, IsVerified = string.IsNullOrEmpty(a.VerificationStatus) ? true : a.VerificationStatus == "accepted", + IsRootAlias = account.Address == a.SendAsEmail, Id = Guid.NewGuid() }).ToList(); - return EntityExtensions.GetFinalAliasList(currentAccount.Aliases, remoteAliases); + return EntityExtensions.GetFinalAliasList(currentAliases, remoteAliases); } } } diff --git a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs index 6ab9ef2d..46116d72 100644 --- a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs @@ -40,6 +40,8 @@ namespace Wino.Core.Integration.Processors /// All folders. Task> GetLocalFoldersAsync(Guid accountId); + Task> GetAccountAliasesAsync(Guid accountId); + Task> GetSynchronizationFoldersAsync(SynchronizationOptions options); Task MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId); @@ -179,6 +181,9 @@ namespace Wino.Core.Integration.Processors => AccountService.UpdateAccountAsync(account); public Task UpdateAccountAliasesAsync(Guid accountId, List aliases) - => AccountService.UpdateAccountAliases(accountId, aliases); + => AccountService.UpdateAccountAliasesAsync(accountId, aliases); + + public Task> GetAccountAliasesAsync(Guid accountId) + => AccountService.GetAccountAliasesAsync(accountId); } } diff --git a/Wino.Core/MenuItems/AccountMenuItem.cs b/Wino.Core/MenuItems/AccountMenuItem.cs index 5cba23d3..c06da2f5 100644 --- a/Wino.Core/MenuItems/AccountMenuItem.cs +++ b/Wino.Core/MenuItems/AccountMenuItem.cs @@ -50,7 +50,7 @@ namespace Wino.Core.MenuItems public string Base64ProfilePicture { get => Parameter.Name; - set => SetProperty(Parameter.ProfilePictureBase64, value, Parameter, (u, n) => u.ProfilePictureBase64 = n); + set => SetProperty(Parameter.Base64ProfilePictureData, value, Parameter, (u, n) => u.Base64ProfilePictureData = n); } public IEnumerable HoldingAccounts => new List { Parameter }; @@ -65,7 +65,7 @@ namespace Wino.Core.MenuItems Parameter = account; AccountName = account.Name; AttentionReason = account.AttentionReason; - Base64ProfilePicture = account.ProfilePictureBase64; + Base64ProfilePicture = account.Base64ProfilePictureData; if (SubMenuItems == null) return; diff --git a/Wino.Core/Services/AccountService.cs b/Wino.Core/Services/AccountService.cs index ab665f30..e029e6bf 100644 --- a/Wino.Core/Services/AccountService.cs +++ b/Wino.Core/Services/AccountService.cs @@ -9,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; @@ -226,43 +227,37 @@ namespace Wino.Core.Services if (account.MergedInboxId != null) account.MergedInbox = await GetMergedInboxInformationAsync(account.MergedInboxId.Value); - // Load aliases - account.Aliases = await GetAccountAliases(account.Id, account.Address); - account.Preferences = await GetAccountPreferencesAsync(account.Id); } return accounts; } - private async Task> GetAccountAliases(Guid accountId, string primaryAccountAddress) + public async Task CreateRootAliasAsync(Guid accountId, string address) { - // By default all accounts must have at least 1 primary alias to create drafts for. - // If there's no alias, create one from the existing account address. Migration doesn't exists to create one for older messages. - - var aliases = await Connection - .Table() - .Where(a => a.AccountId == accountId) - .ToListAsync() - .ConfigureAwait(false); - - if (!aliases.Any()) + var rootAlias = new MailAccountAlias() { - var primaryAccountAlias = new MailAccountAlias() - { - Id = Guid.NewGuid(), - AccountId = accountId, - IsPrimary = true, - AliasAddress = primaryAccountAddress, - ReplyToAddress = primaryAccountAddress, - IsVerified = true, - }; + AccountId = accountId, + AliasAddress = address, + IsPrimary = true, + IsRootAlias = true, + IsVerified = true, + ReplyToAddress = address, + Id = Guid.NewGuid() + }; - await Connection.InsertAsync(primaryAccountAlias).ConfigureAwait(false); - aliases.Add(primaryAccountAlias); - } + await Connection.InsertAsync(rootAlias).ConfigureAwait(false); - return aliases; + Log.Information("Created root alias for the account {AccountId}", accountId); + } + + public async Task> GetAccountAliasesAsync(Guid accountId) + { + var query = new Query(nameof(MailAccountAlias)) + .Where(nameof(MailAccountAlias.AccountId), accountId) + .OrderByDesc(nameof(MailAccountAlias.IsRootAlias)); + + return await Connection.QueryAsync(query.GetRawQuery()).ConfigureAwait(false); } private Task GetMergedInboxInformationAsync(Guid mergedInboxId) @@ -277,6 +272,7 @@ namespace Wino.Core.Services await Connection.Table().Where(a => a.AccountId == account.Id).DeleteAsync(); await Connection.Table().DeleteAsync(a => a.MailAccountId == account.Id); await Connection.Table().DeleteAsync(a => a.MailAccountId == account.Id); + await Connection.Table().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. @@ -327,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 GetAccountAsync(Guid accountId) { var account = await Connection.Table().FirstOrDefaultAsync(a => a.Id == accountId); @@ -359,7 +368,7 @@ namespace Wino.Core.Services ReportUIChange(new AccountUpdatedMessage(account)); } - public async Task UpdateAccountAliases(Guid accountId, List aliases) + public async Task UpdateAccountAliasesAsync(Guid accountId, List aliases) { // Delete existing ones. await Connection.Table().DeleteAsync(a => a.AccountId == accountId).ConfigureAwait(false); @@ -371,6 +380,17 @@ namespace Wino.Core.Services } } + 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); @@ -424,7 +444,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); } diff --git a/Wino.Core/Synchronizers/BaseSynchronizer.cs b/Wino.Core/Synchronizers/BaseSynchronizer.cs index 055a4bb4..11c107c6 100644 --- a/Wino.Core/Synchronizers/BaseSynchronizer.cs +++ b/Wino.Core/Synchronizers/BaseSynchronizer.cs @@ -13,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; @@ -71,10 +72,16 @@ namespace Wino.Core.Synchronizers public abstract Task ExecuteNativeRequestsAsync(IEnumerable> batchedRequests, CancellationToken cancellationToken = default); /// - /// Refreshed remote mail account profile if possible. - /// Aliases, profile pictures, mailbox settings will be handled in this step. + /// Refreshes remote mail account profile if possible. + /// Profile picture, sender name and mailbox settings (todo) will be handled in this step. /// - protected virtual Task SynchronizeProfileInformationAsync() => Task.CompletedTask; + public virtual Task SynchronizeProfileInformationAsync() => default; + + /// + /// Refreshes the aliases of the account. + /// Only available for Gmail right now. + /// + protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask; /// /// Returns the base64 encoded profile picture of the account from the given URL. @@ -100,6 +107,33 @@ namespace Wino.Core.Synchronizers /// Synchronization result that contains summary of the sync. protected abstract Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default); + /// + /// Safely updates account's profile information. + /// Database changes are reflected after this call. + /// Null returns mean that the operation failed. + /// + private async Task SynchronizeProfileInformationInternalAsync() + { + try + { + var profileInformation = await SynchronizeProfileInformationAsync(); + + if (profileInformation != null) + { + Account.SenderName = profileInformation.SenderName; + Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData; + } + + return profileInformation; + } + catch (Exception ex) + { + Log.Error(ex, "Failed to update profile information for account '{Name}'", Account.Name); + } + + return null; + } + /// /// Batches network requests, executes them, and does the needed synchronization after the batch request execution. /// @@ -139,12 +173,16 @@ namespace Wino.Core.Synchronizers await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken); - if (options.Type == SynchronizationType.Full) + if (options.Type == SynchronizationType.UpdateProfile) { - // Refresh profile information and mailbox settings on full synchronization. + // Refresh profile information on full synchronization. // Exceptions here is not critical. Therefore, they are ignored. - await SynchronizeProfileInformationAsync(); + var newprofileInformation = await SynchronizeProfileInformationInternalAsync(); + + if (newprofileInformation == null) return SynchronizationResult.Failed; + + return SynchronizationResult.Completed(null, newprofileInformation); } // Let servers to finish their job. Sometimes the servers doesn't respond immediately. diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs index 57da147d..8dd48eab 100644 --- a/Wino.Core/Synchronizers/GmailSynchronizer.cs +++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs @@ -19,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; @@ -68,67 +69,45 @@ namespace Wino.Core.Synchronizers public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) => _googleHttpClient; - protected override async Task SynchronizeProfileInformationAsync() + public override async Task SynchronizeProfileInformationAsync() { - // Gmail profile info synchronizes Sender Name, Alias and Profile Picture. + var profileRequest = _peopleService.People.Get("people/me"); + profileRequest.PersonFields = "names,photos"; - try + 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)) { - var profileRequest = _peopleService.People.Get("people/me"); - profileRequest.PersonFields = "names,photos"; - - string senderName = Account.SenderName, base64ProfilePicture = Account.ProfilePictureBase64; - - 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); - } - - bool shouldUpdateAccountProfile = (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName) - || (!string.IsNullOrEmpty(profilePicture) && Account.ProfilePictureBase64 != base64ProfilePicture); - - if (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName) - { - Account.SenderName = senderName; - } - - if (!string.IsNullOrEmpty(profilePicture) && Account.ProfilePictureBase64 != base64ProfilePicture) - { - Account.ProfilePictureBase64 = base64ProfilePicture; - } - - // Sync aliases - - var sendAsListRequest = _gmailService.Users.Settings.SendAs.List("me"); - var sendAsListResponse = await sendAsListRequest.ExecuteAsync(); - - var updatedAliases = sendAsListResponse.GetMailAliases(Account); - - bool shouldUpdateAliases = - Account.Aliases.Any(a => updatedAliases.Any(b => a.Id == b.Id) == false) || - updatedAliases.Any(a => Account.Aliases.Any(b => a.Id == b.Id) == false); - - if (shouldUpdateAliases) - { - Account.Aliases = updatedAliases; - - await _gmailChangeProcessor.UpdateAccountAliasesAsync(Account.Id, updatedAliases); - } - - if (shouldUpdateAccountProfile) - { - await _gmailChangeProcessor.UpdateAccountAsync(Account).ConfigureAwait(false); - } + base64ProfilePicture = await GetProfilePictureBase64EncodedAsync(profilePicture).ConfigureAwait(false); } - catch (Exception ex) + + return new ProfileInformation(senderName, base64ProfilePicture); + } + + protected override async Task SynchronizeAliasesAsync() + { + // Sync aliases + + var sendAsListRequest = _gmailService.Users.Settings.SendAs.List("me"); + var sendAsListResponse = await sendAsListRequest.ExecuteAsync(); + + var localAliases = await _gmailChangeProcessor.GetAccountAliasesAsync(Account.Id).ConfigureAwait(false); + + var updatedAliases = sendAsListResponse.GetMailAliases(localAliases, Account); + + bool shouldUpdateAliases = + localAliases.Any(a => updatedAliases.Any(b => a.Id == b.Id) == false) || + updatedAliases.Any(a => localAliases.Any(b => a.Id == b.Id) == false); + + if (shouldUpdateAliases) { - Logger.Error(ex, "Error while synchronizing profile information for {Name}", Account.Name); + await _gmailChangeProcessor.UpdateAccountAliasesAsync(Account.Id, updatedAliases); } } diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs index 7080e9c7..90bc66f5 100644 --- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs @@ -23,6 +23,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; @@ -479,70 +480,28 @@ namespace Wino.Core.Synchronizers /// Base64 encoded profile picture. private async Task GetUserProfilePictureAsync() { - try - { - var photoStream = await _graphClient.Me.Photos["48x48"].Content.GetAsync(); + var photoStream = await _graphClient.Me.Photos["48x48"].Content.GetAsync(); - using var memoryStream = new MemoryStream(); - await photoStream.CopyToAsync(memoryStream); - var byteArray = memoryStream.ToArray(); + using var memoryStream = new MemoryStream(); + await photoStream.CopyToAsync(memoryStream); + var byteArray = memoryStream.ToArray(); - return Convert.ToBase64String(byteArray); - } - catch (Exception ex) - { - Log.Error(ex, "Error occurred while getting user profile picture."); - return string.Empty; - } + return Convert.ToBase64String(byteArray); } private async Task GetSenderNameAsync() { - try - { - var userInfo = await _graphClient.Users["me"].GetAsync(); + var userInfo = await _graphClient.Users["me"].GetAsync(); - return userInfo.DisplayName; - } - catch (Exception ex) - { - Log.Error(ex, "Failed to get sender name."); - return string.Empty; - } + return userInfo.DisplayName; } - protected override async Task SynchronizeProfileInformationAsync() + public override async Task SynchronizeProfileInformationAsync() { - try - { - // Outlook profile info synchronizes Sender Name and Profile Picture. - string senderName = Account.SenderName, base64ProfilePicture = Account.ProfilePictureBase64; + var profilePictureData = await GetUserProfilePictureAsync().ConfigureAwait(false); + var senderName = await GetSenderNameAsync().ConfigureAwait(false); - var profilePictureData = await GetUserProfilePictureAsync().ConfigureAwait(false); - senderName = await GetSenderNameAsync().ConfigureAwait(false); - - bool shouldUpdateAccountProfile = (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName) - || (!string.IsNullOrEmpty(profilePictureData) && Account.ProfilePictureBase64 != base64ProfilePicture); - - if (!string.IsNullOrEmpty(profilePictureData) && Account.ProfilePictureBase64 != profilePictureData) - { - Account.ProfilePictureBase64 = profilePictureData; - } - - if (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName) - { - Account.SenderName = senderName; - } - - if (shouldUpdateAccountProfile) - { - await _outlookChangeProcessor.UpdateAccountAsync(Account).ConfigureAwait(false); - } - } - catch (Exception ex) - { - Log.Error(ex, "Failed to synchronize profile information for {Name}", Account.Name); - } + return new ProfileInformation(senderName, profilePictureData); } #region Mail Integration diff --git a/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs b/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs index eb8e0fc0..306f3bff 100644 --- a/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs +++ b/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs @@ -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); diff --git a/Wino.Mail.ViewModels/AccountManagementViewModel.cs b/Wino.Mail.ViewModels/AccountManagementViewModel.cs index e503b99b..367ca141 100644 --- a/Wino.Mail.ViewModels/AccountManagementViewModel.cs +++ b/Wino.Mail.ViewModels/AccountManagementViewModel.cs @@ -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,59 @@ namespace Wino.Mail.ViewModels await _accountService.CreateAccountAsync(createdAccount, tokenInformation, customServerInformation); // Local account has been created. - // Create new synchronizer and start synchronization. + + // Start profile information synchronization. + // Profile info is not updated in the database yet. + + var profileSyncOptions = new SynchronizationOptions() + { + AccountId = createdAccount.Id, + Type = SynchronizationType.UpdateProfile + }; + + var profileSynchronizationResponse = await _winoServerConnectionManager.GetResponseAsync(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(new NewSynchronizationRequested(options, SynchronizationSource.Client)); + var folderSynchronizationResponse = await _winoServerConnectionManager.GetResponseAsync(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); + // Create root primary alias for the account. + // This is the first alias for the account and it's primary. - if (!isInboxAvailable) - throw new Exception(Translator.Exception_InboxNotAvailable); + 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)); diff --git a/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs new file mode 100644 index 00000000..234cf49f --- /dev/null +++ b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs @@ -0,0 +1,115 @@ +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; + +namespace Wino.Mail.ViewModels +{ + public partial class AliasManagementPageViewModel : BaseViewModel + { + private readonly IAccountService _accountService; + + public MailAccount Account { get; set; } + + [ObservableProperty] + private List accountAliases = []; + + public AliasManagementPageViewModel(IDialogService dialogService, IAccountService accountService) : base(dialogService) + { + _accountService = accountService; + } + + 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 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.ReplyToAddress) || !EmailValidator.Validate(newAlias.AliasAddress)) + { + 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(); + } + } +} diff --git a/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs b/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs index c90b81aa..9ba7f782 100644 --- a/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs @@ -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; } } } diff --git a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj index 946a3bf8..82c61360 100644 --- a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj +++ b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj @@ -7,6 +7,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Wino.Mail/App.xaml b/Wino.Mail/App.xaml index 64b6c59e..57cb3f20 100644 --- a/Wino.Mail/App.xaml +++ b/Wino.Mail/App.xaml @@ -1,9 +1,10 @@ - + @@ -48,6 +49,14 @@ + + + -