diff --git a/Wino.Core.Domain/Entities/MailAccount.cs b/Wino.Core.Domain/Entities/MailAccount.cs index c1bc43a1..e668a6a3 100644 --- a/Wino.Core.Domain/Entities/MailAccount.cs +++ b/Wino.Core.Domain/Entities/MailAccount.cs @@ -78,18 +78,20 @@ namespace Wino.Core.Domain.Entities [Ignore] public CustomServerInformation ServerInformation { get; set; } - /// - /// Gets or sets the aliases of the account. - /// 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; } - /// /// Account preferences. /// [Ignore] public MailAccountPreferences Preferences { get; set; } + + /// + /// Gets whether the account can perform ProfileInformation sync type. + /// + public bool IsProfileInfoSyncSupported => ProviderType == MailProviderType.Outlook || ProviderType == MailProviderType.Office365 || ProviderType == MailProviderType.Gmail; + + /// + /// Gets whether the account can perform AliasInformation sync type. + /// + public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail; } } diff --git a/Wino.Core.Domain/Entities/MailAccountAlias.cs b/Wino.Core.Domain/Entities/MailAccountAlias.cs index 8d103744..fa60b42b 100644 --- a/Wino.Core.Domain/Entities/MailAccountAlias.cs +++ b/Wino.Core.Domain/Entities/MailAccountAlias.cs @@ -1,22 +1,10 @@ using System; -using System.Collections.Generic; using SQLite; namespace Wino.Core.Domain.Entities { - public class MailAccountAlias + public class RemoteAccountAlias { - /// - /// Unique Id for the alias. - /// - [PrimaryKey] - public Guid Id { get; set; } - - /// - /// Account id that this alias is attached to. - /// - public Guid AccountId { get; set; } - /// /// Display address of the alias. /// @@ -32,13 +20,6 @@ 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. @@ -47,37 +28,30 @@ namespace Wino.Core.Domain.Entities /// public bool IsVerified { 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; } + } + + public class MailAccountAlias : RemoteAccountAlias + { + /// + /// Unique Id for the alias. + /// + [PrimaryKey] + public Guid Id { get; set; } + + /// + /// Account id that this alias is attached to. + /// + public Guid AccountId { get; set; } + /// /// Root aliases can't be deleted. /// public bool CanDelete => !IsRootAlias; - - public override bool Equals(object obj) - { - if (obj == null || GetType() != obj.GetType()) - return false; - - var other = (MailAccountAlias)obj; - return other != null && - AccountId == other.AccountId && - AliasAddress == other.AliasAddress && - ReplyToAddress == other.ReplyToAddress && - IsPrimary == other.IsPrimary && - IsVerified == other.IsVerified && - IsRootAlias == other.IsRootAlias; - } - - public override int GetHashCode() - { - 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/SynchronizationType.cs b/Wino.Core.Domain/Enums/SynchronizationType.cs index 927016d6..a8cfba1d 100644 --- a/Wino.Core.Domain/Enums/SynchronizationType.cs +++ b/Wino.Core.Domain/Enums/SynchronizationType.cs @@ -4,9 +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 } } diff --git a/Wino.Core.Domain/Extensions/EntityExtensions.cs b/Wino.Core.Domain/Extensions/EntityExtensions.cs deleted file mode 100644 index b078ad99..00000000 --- a/Wino.Core.Domain/Extensions/EntityExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Wino.Core.Domain.Entities; - -namespace Wino.Core.Domain.Extensions -{ - public static class EntityExtensions - { - public static List GetFinalAliasList(List localAliases, List networkAliases) - { - var finalAliases = new List(); - - var networkAliasDict = networkAliases.ToDictionary(a => a, a => a); - - // Handle updating and retaining existing aliases - foreach (var localAlias in localAliases) - { - if (networkAliasDict.TryGetValue(localAlias, out var networkAlias)) - { - // If alias exists in both lists, update it with the network alias (preserving Id from local) - networkAlias.Id = localAlias.Id; // Preserve the local Id - finalAliases.Add(networkAlias); - networkAliasDict.Remove(localAlias); // Remove from dictionary to track what's been handled - } - // If the alias isn't in the network list, it's considered deleted and not added to finalAliases - } - - // Add new aliases that were not in the local list - finalAliases.AddRange(networkAliasDict.Values); - - return finalAliases; - } - } -} diff --git a/Wino.Core.Domain/Interfaces/IAccountService.cs b/Wino.Core.Domain/Interfaces/IAccountService.cs index e7196653..8c800220 100644 --- a/Wino.Core.Domain/Interfaces/IAccountService.cs +++ b/Wino.Core.Domain/Interfaces/IAccountService.cs @@ -139,5 +139,12 @@ namespace Wino.Core.Domain.Interfaces /// Account id. /// Address to create root primary alias from. Task CreateRootAliasAsync(Guid accountId, string address); + + /// + /// Will compare local-remote aliases and update the local ones or add/delete new ones. + /// + /// Remotely fetched basic alias info from synchronizer. + /// Account to update remote aliases for.. + Task UpdateRemoteAliasInformationAsync(MailAccount account, List remoteAccountAliases); } } diff --git a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs index e6ccd2a1..00b1ec6c 100644 --- a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs +++ b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs @@ -49,7 +49,7 @@ namespace Wino.Core.Domain.Interfaces /// Sender name and /// /// - Task SynchronizeProfileInformationAsync(); + Task GetProfileInformationAsync(); /// /// Downloads a single MIME message from the server and saves it to disk. diff --git a/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs b/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs index 885a4b9b..0e98844a 100644 --- a/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs +++ b/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs @@ -25,7 +25,12 @@ namespace Wino.Core.Domain.Models.Synchronization public static SynchronizationResult Empty => new() { CompletedState = SynchronizationCompletedState.Success }; public static SynchronizationResult Completed(IEnumerable downloadedMessages, ProfileInformation profileInformation = null) - => new() { DownloadedMessages = downloadedMessages, ProfileInformation = profileInformation, CompletedState = SynchronizationCompletedState.Success }; + => 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 b77392dc..696f41ef 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -22,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", @@ -128,6 +130,7 @@ "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_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information", "Exception_GoogleAuthCallbackNull": "Callback uri is null on activation.", "Exception_GoogleAuthCorruptedCode": "Corrupted authorization response.", diff --git a/Wino.Core.Domain/Translator.Designer.cs b/Wino.Core.Domain/Translator.Designer.cs index a20c9711..e37aedff 100644 --- a/Wino.Core.Domain/Translator.Designer.cs +++ b/Wino.Core.Domain/Translator.Designer.cs @@ -133,6 +133,16 @@ namespace Wino.Core.Domain /// public static string Buttons_AddAccount => Resources.GetTranslatedString(@"Buttons_AddAccount"); + /// + /// Add New Alias + /// + public static string Buttons_AddNewAlias => Resources.GetTranslatedString(@"Buttons_AddNewAlias"); + + /// + /// Synchronize Aliases + /// + public static string Buttons_SyncAliases => Resources.GetTranslatedString(@"Buttons_SyncAliases"); + /// /// Apply Theme /// @@ -663,6 +673,11 @@ namespace Wino.Core.Domain /// public static string Exception_FailedToSynchronizeFolders => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeFolders"); + /// + /// Failed to synchronize aliases + /// + public static string Exception_FailedToSynchronizeAliases => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeAliases"); + /// /// Failed to synchronize profile information /// diff --git a/Wino.Core/Extensions/GoogleIntegratorExtensions.cs b/Wino.Core/Extensions/GoogleIntegratorExtensions.cs index ed290d13..1aa319e3 100644 --- a/Wino.Core/Extensions/GoogleIntegratorExtensions.cs +++ b/Wino.Core/Extensions/GoogleIntegratorExtensions.cs @@ -6,7 +6,6 @@ using Google.Apis.Gmail.v1.Data; using MimeKit; using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Extensions; namespace Wino.Core.Extensions { @@ -205,22 +204,16 @@ namespace Wino.Core.Extensions }; } - public static List GetMailAliases(this ListSendAsResponse response, List currentAliases, MailAccount account) + public static List GetRemoteAliases(this ListSendAsResponse response) { - if (response == null || response.SendAs == null) return currentAliases; - - var remoteAliases = response.SendAs.Select(a => new MailAccountAlias() + return response?.SendAs?.Select(a => new RemoteAccountAlias() { - AccountId = account.Id, AliasAddress = a.SendAsEmail, + IsRootAlias = a.IsDefault.GetValueOrDefault(), IsPrimary = a.IsPrimary.GetValueOrDefault(), - 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() + ReplyToAddress = a.ReplyToAddress, + IsVerified = a.VerificationStatus == "accepted" || a.IsDefault.GetValueOrDefault(), }).ToList(); - - return EntityExtensions.GetFinalAliasList(currentAliases, remoteAliases); } } } diff --git a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs index 46116d72..7c39fcb5 100644 --- a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs @@ -40,7 +40,6 @@ namespace Wino.Core.Integration.Processors /// All folders. Task> GetLocalFoldersAsync(Guid accountId); - Task> GetAccountAliasesAsync(Guid accountId); Task> GetSynchronizationFoldersAsync(SynchronizationOptions options); @@ -48,7 +47,7 @@ namespace Wino.Core.Integration.Processors Task UpdateFolderLastSyncDateAsync(Guid folderId); Task> GetExistingFoldersAsync(Guid accountId); - Task UpdateAccountAliasesAsync(Guid accountId, List aliases); + Task UpdateRemoteAliasInformationAsync(MailAccount account, List remoteAccountAliases); } public interface IGmailChangeProcessor : IDefaultChangeProcessor @@ -180,10 +179,7 @@ namespace Wino.Core.Integration.Processors public Task UpdateAccountAsync(MailAccount account) => AccountService.UpdateAccountAsync(account); - public Task UpdateAccountAliasesAsync(Guid accountId, List aliases) - => AccountService.UpdateAccountAliasesAsync(accountId, aliases); - - public Task> GetAccountAliasesAsync(Guid accountId) - => AccountService.GetAccountAliasesAsync(accountId); + public Task UpdateRemoteAliasInformationAsync(MailAccount account, List remoteAccountAliases) + => AccountService.UpdateRemoteAliasInformationAsync(account, remoteAccountAliases); } } diff --git a/Wino.Core/Services/AccountService.cs b/Wino.Core/Services/AccountService.cs index e029e6bf..163c099e 100644 --- a/Wino.Core/Services/AccountService.cs +++ b/Wino.Core/Services/AccountService.cs @@ -380,6 +380,69 @@ namespace Wino.Core.Services } } + public async Task UpdateRemoteAliasInformationAsync(MailAccount account, List 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. diff --git a/Wino.Core/Synchronizers/BaseSynchronizer.cs b/Wino.Core/Synchronizers/BaseSynchronizer.cs index 11c107c6..39d53f66 100644 --- a/Wino.Core/Synchronizers/BaseSynchronizer.cs +++ b/Wino.Core/Synchronizers/BaseSynchronizer.cs @@ -75,7 +75,7 @@ namespace Wino.Core.Synchronizers /// Refreshes remote mail account profile if possible. /// Profile picture, sender name and mailbox settings (todo) will be handled in this step. /// - public virtual Task SynchronizeProfileInformationAsync() => default; + public virtual Task GetProfileInformationAsync() => default; /// /// Refreshes the aliases of the account. @@ -110,28 +110,18 @@ namespace Wino.Core.Synchronizers /// /// 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(); + var profileInformation = await GetProfileInformationAsync(); - if (profileInformation != null) - { - Account.SenderName = profileInformation.SenderName; - Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData; - } - - return profileInformation; - } - catch (Exception ex) + if (profileInformation != null) { - Log.Error(ex, "Failed to update profile information for account '{Name}'", Account.Name); + Account.SenderName = profileInformation.SenderName; + Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData; } - return null; + return profileInformation; } /// @@ -173,16 +163,46 @@ namespace Wino.Core.Synchronizers await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken); + // Handle special synchronization types. + + // Profile information sync. if (options.Type == SynchronizationType.UpdateProfile) { - // Refresh profile information on full synchronization. - // Exceptions here is not critical. Therefore, they are ignored. + if (!Account.IsProfileInfoSyncSupported) return SynchronizationResult.Empty; - var newprofileInformation = await SynchronizeProfileInformationInternalAsync(); + ProfileInformation newProfileInformation = null; - if (newprofileInformation == null) return SynchronizationResult.Failed; + try + { + newProfileInformation = await SynchronizeProfileInformationInternalAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to update profile information for {Name}", Account.Name); - return SynchronizationResult.Completed(null, newprofileInformation); + 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. diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs index 8dd48eab..d0cf7b2f 100644 --- a/Wino.Core/Synchronizers/GmailSynchronizer.cs +++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs @@ -69,7 +69,7 @@ namespace Wino.Core.Synchronizers public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) => _googleHttpClient; - public override async Task SynchronizeProfileInformationAsync() + public override async Task GetProfileInformationAsync() { var profileRequest = _peopleService.People.Get("people/me"); profileRequest.PersonFields = "names,photos"; @@ -92,23 +92,11 @@ namespace Wino.Core.Synchronizers protected override async Task SynchronizeAliasesAsync() { - // Sync aliases - var sendAsListRequest = _gmailService.Users.Settings.SendAs.List("me"); var sendAsListResponse = await sendAsListRequest.ExecuteAsync(); + var remoteAliases = sendAsListResponse.GetRemoteAliases(); - 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) - { - await _gmailChangeProcessor.UpdateAccountAliasesAsync(Account.Id, updatedAliases); - } + await _gmailChangeProcessor.UpdateRemoteAliasInformationAsync(Account, remoteAliases).ConfigureAwait(false); } protected override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) diff --git a/Wino.Core/Synchronizers/ImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSynchronizer.cs index 5624edf4..e0ef6810 100644 --- a/Wino.Core/Synchronizers/ImapSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSynchronizer.cs @@ -922,7 +922,7 @@ namespace Wino.Core.Synchronizers } // In case of the high input, we'll batch them by 50 to reflect changes quickly. - var batchedMissingMailIds = missingMailIds.Batch(50).Select(a => new UniqueIdSet(a, SortOrder.Descending)); + var batchedMissingMailIds = missingMailIds.Batch(50).Select(a => new UniqueIdSet(a, SortOrder.Ascending)); foreach (var batchMissingMailIds in batchedMissingMailIds) { diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs index 90bc66f5..964d5c5b 100644 --- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs @@ -496,7 +496,7 @@ namespace Wino.Core.Synchronizers return userInfo.DisplayName; } - public override async Task SynchronizeProfileInformationAsync() + public override async Task GetProfileInformationAsync() { var profilePictureData = await GetUserProfilePictureAsync().ConfigureAwait(false); var senderName = await GetSenderNameAsync().ConfigureAwait(false); diff --git a/Wino.Mail.ViewModels/AccountManagementViewModel.cs b/Wino.Mail.ViewModels/AccountManagementViewModel.cs index 367ca141..a1dee843 100644 --- a/Wino.Mail.ViewModels/AccountManagementViewModel.cs +++ b/Wino.Mail.ViewModels/AccountManagementViewModel.cs @@ -206,26 +206,29 @@ namespace Wino.Mail.ViewModels // Local account has been created. - // Start profile information synchronization. - // Profile info is not updated in the database yet. - - var profileSyncOptions = new SynchronizationOptions() + if (createdAccount.ProviderType != MailProviderType.IMAP4) { - AccountId = createdAccount.Id, - Type = SynchronizationType.UpdateProfile - }; + // Start profile information synchronization. + // It's only available for Outlook and Gmail synchronizers. - var profileSynchronizationResponse = await _winoServerConnectionManager.GetResponseAsync(new NewSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client)); + var profileSyncOptions = new SynchronizationOptions() + { + AccountId = createdAccount.Id, + Type = SynchronizationType.UpdateProfile + }; - var profileSynchronizationResult = profileSynchronizationResponse.Data; + var profileSynchronizationResponse = await _winoServerConnectionManager.GetResponseAsync(new NewSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client)); - if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success) - throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation); + var profileSynchronizationResult = profileSynchronizationResponse.Data; - createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName; - createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData; + if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success) + throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation); - await _accountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation); + createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName; + createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData; + + await _accountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation); + } if (creationDialog is ICustomServerAccountCreationDialog customServerAccountCreationDialog) customServerAccountCreationDialog.ShowPreparingFolders(); @@ -246,10 +249,29 @@ namespace Wino.Mail.ViewModels if (folderSynchronizationResult.CompletedState != SynchronizationCompletedState.Success) throw new Exception(Translator.Exception_FailedToSynchronizeFolders); - // Create root primary alias for the account. - // This is the first alias for the account and it's primary. + if (createdAccount.IsAliasSyncSupported) + { + // Try to synchronize aliases for the account. - await _accountService.CreateRootAliasAsync(createdAccount.Id, createdAccount.Address); + var aliasSyncOptions = new SynchronizationOptions() + { + AccountId = createdAccount.Id, + Type = SynchronizationType.Alias + }; + + var aliasSyncResponse = await _winoServerConnectionManager.GetResponseAsync(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. diff --git a/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs index 234cf49f..3da9e8d0 100644 --- a/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs +++ b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs @@ -10,21 +10,30 @@ 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; - - public MailAccount Account { get; set; } + private readonly IWinoServerConnectionManager _winoServerConnectionManager; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanSynchronizeAliases))] + private MailAccount account; [ObservableProperty] private List accountAliases = []; - public AliasManagementPageViewModel(IDialogService dialogService, IAccountService accountService) : base(dialogService) + 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) @@ -58,6 +67,25 @@ namespace Wino.Mail.ViewModels 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(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() { diff --git a/Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs b/Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs index 8cdacde7..3d1c5e3a 100644 --- a/Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs +++ b/Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs @@ -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; diff --git a/Wino.Mail/Dialogs/NewAccountDialog.xaml b/Wino.Mail/Dialogs/NewAccountDialog.xaml index 0cf082e2..7d3d1d02 100644 --- a/Wino.Mail/Dialogs/NewAccountDialog.xaml +++ b/Wino.Mail/Dialogs/NewAccountDialog.xaml @@ -71,7 +71,7 @@ + - + @@ -36,29 +34,26 @@ - + - - + + @@ -67,18 +62,16 @@ - + - + @@ -90,12 +83,11 @@ - - - + + @@ -136,54 +126,46 @@ - - + + - + - + - + - @@ -192,43 +174,39 @@ - + - + - + - + @@ -243,29 +221,24 @@ - - - - + + + + - + diff --git a/Wino.Mail/Views/Settings/AliasManagementPage.xaml b/Wino.Mail/Views/Settings/AliasManagementPage.xaml index 61ce57b5..4da46c04 100644 --- a/Wino.Mail/Views/Settings/AliasManagementPage.xaml +++ b/Wino.Mail/Views/Settings/AliasManagementPage.xaml @@ -1,22 +1,21 @@ - + @@ -39,39 +38,34 @@ - + - + - + - + - + + - - - - - - - - - - - - + + + + + + + + +