using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; using CommunityToolkit.Mvvm.Messaging; using Serilog; using Wino.Core.Domain; using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Misc; using Wino.Messaging.Client.Calendar; using Wino.Messaging.Client.Accounts; using Wino.Messaging.UI; namespace Wino.Services; public class AccountService : BaseDatabaseService, IAccountService { public IAuthenticator ExternalAuthenticationAuthenticator { get; set; } private readonly ISignatureService _signatureService; private readonly IAuthenticationProvider _authenticationProvider; private readonly IMimeFileService _mimeFileService; private readonly IPreferencesService _preferencesService; private readonly IContactPictureFileService _contactPictureFileService; private readonly ILogger _logger = Log.ForContext(); public AccountService(IDatabaseService databaseService, ISignatureService signatureService, IAuthenticationProvider authenticationProvider, IMimeFileService mimeFileService, IPreferencesService preferencesService, IContactPictureFileService contactPictureFileService) : base(databaseService) { _signatureService = signatureService; _authenticationProvider = authenticationProvider; _mimeFileService = mimeFileService; _preferencesService = preferencesService; _contactPictureFileService = contactPictureFileService; } public async Task ClearAccountAttentionAsync(Guid accountId) { var account = await GetAccountAsync(accountId); Guard.IsNotNull(account); account.AttentionReason = AccountAttentionReason.None; await UpdateAccountAsync(account); } public async Task UpdateMergedInboxAsync(Guid mergedInboxId, IEnumerable linkedAccountIds) { // First, remove all accounts from merged inbox. await Connection.ExecuteAsync("UPDATE MailAccount SET MergedInboxId = NULL WHERE MergedInboxId = ?", mergedInboxId); // Then, add new accounts to merged inbox. var accountIdList = linkedAccountIds.ToList(); var placeholders = string.Join(",", accountIdList.Select(_ => "?")); var sql = $"UPDATE MailAccount SET MergedInboxId = ? WHERE Id IN ({placeholders})"; var parameters = new List { mergedInboxId }; parameters.AddRange(accountIdList.Cast()); await Connection.ExecuteAsync(sql, parameters.ToArray()); WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested()); } public async Task UpdateSyncIdentifierRawAsync(Guid accountId, string syncIdentifier) { await Connection.ExecuteAsync("UPDATE MailAccount SET SynchronizationDeltaIdentifier = ? WHERE Id = ?", syncIdentifier, accountId); return syncIdentifier; } public async Task UnlinkMergedInboxAsync(Guid mergedInboxId) { var mergedInbox = await Connection.Table().FirstOrDefaultAsync(a => a.Id == mergedInboxId).ConfigureAwait(false); if (mergedInbox == null) { _logger.Warning("Could not find merged inbox with id {MergedInboxId}", mergedInboxId); return; } await Connection.ExecuteAsync("UPDATE MailAccount SET MergedInboxId = NULL WHERE MergedInboxId = ?", mergedInboxId).ConfigureAwait(false); await Connection.DeleteAsync(mergedInbox.Id).ConfigureAwait(false); // Change the startup entity id if it was the merged inbox. // Take the first account as startup account. if (_preferencesService.StartupEntityId == mergedInboxId) { var firstAccount = await Connection.Table().FirstOrDefaultAsync(); if (firstAccount != null) { _preferencesService.StartupEntityId = firstAccount.Id; } else { _preferencesService.StartupEntityId = null; } } WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested()); } public async Task CreateMergeAccountsAsync(MergedInbox mergedInbox, IEnumerable accountsToMerge) { if (mergedInbox == null) return; // 0. Give the merged inbox a new Guid. mergedInbox.Id = Guid.NewGuid(); var accountFolderDictionary = new Dictionary>(); // 1. Make all folders in the accounts unsticky. We will stick them based on common special folder types. foreach (var account in accountsToMerge) { var accountFolderList = new List(); var folders = await Connection.Table().Where(a => a.MailAccountId == account.Id).ToListAsync(); foreach (var folder in folders) { accountFolderList.Add(folder); folder.IsSticky = false; await Connection.UpdateAsync(folder, typeof(MailItemFolder)); } accountFolderDictionary.Add(account, accountFolderList); } // 2. Find the common special folders and stick them. // Only following types will be considered as common special folder. SpecialFolderType[] commonSpecialTypes = [ SpecialFolderType.Inbox, SpecialFolderType.Sent, SpecialFolderType.Draft, SpecialFolderType.Archive, SpecialFolderType.Junk, SpecialFolderType.Deleted ]; foreach (var type in commonSpecialTypes) { var isCommonType = accountFolderDictionary .Select(a => a.Value) .Where(a => a.Any(a => a.SpecialFolderType == type)) .Count() == accountsToMerge.Count(); if (isCommonType) { foreach (var account in accountsToMerge) { var folder = accountFolderDictionary[account].FirstOrDefault(a => a.SpecialFolderType == type); if (folder != null) { folder.IsSticky = true; await Connection.UpdateAsync(folder, typeof(MailItemFolder)); } } } } // 3. Insert merged inbox and assign accounts. await Connection.InsertAsync(mergedInbox, typeof(MergedInbox)); foreach (var account in accountsToMerge) { account.MergedInboxId = mergedInbox.Id; await Connection.UpdateAsync(account, typeof(MailAccount)); } WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested()); } public async Task RenameMergedAccountAsync(Guid mergedInboxId, string newName) { await Connection.ExecuteAsync("UPDATE MergedInbox SET Name = ? WHERE Id = ?", newName, mergedInboxId); ReportUIChange(new MergedInboxRenamed(mergedInboxId, newName)); } public async Task FixTokenIssuesAsync(Guid accountId) { var account = await Connection.Table().FirstOrDefaultAsync(a => a.Id == accountId); if (account == null) return; var authenticator = _authenticationProvider.GetAuthenticator(account.ProviderType); // This will re-generate token with interactive authentication // New authentication will include calendar scopes var token = await authenticator.GenerateTokenInformationAsync(account); Guard.IsNotNull(token); await UpdateAccountAsync(account); } private Task GetAccountPreferencesAsync(Guid accountId) => Connection.Table().FirstOrDefaultAsync(a => a.AccountId == accountId); public async Task> GetAccountsAsync() { var accounts = await Connection.Table().OrderBy(a => a.Order).ToListAsync(); foreach (var account in accounts) { // Load IMAP server configuration. if (account.ProviderType == MailProviderType.IMAP4) account.ServerInformation = await GetAccountCustomServerInformationAsync(account.Id); // Load MergedInbox information. if (account.MergedInboxId != null) account.MergedInbox = await GetMergedInboxInformationAsync(account.MergedInboxId.Value); account.Preferences = await GetAccountPreferencesAsync(account.Id); } return accounts; } public async Task AccountNameExistsAsync(string name, Guid? excludedAccountId = null) { var normalizedName = name?.Trim(); if (string.IsNullOrWhiteSpace(normalizedName)) return false; var accounts = await Connection.Table().ToListAsync().ConfigureAwait(false); return accounts.Any(account => account.Id != excludedAccountId && string.Equals(account.Name?.Trim(), normalizedName, StringComparison.OrdinalIgnoreCase)); } public async Task AccountAddressExistsAsync(string address, Guid? excludedAccountId = null) { var normalizedAddress = address?.Trim(); if (string.IsNullOrWhiteSpace(normalizedAddress)) return false; var accounts = await Connection.Table().ToListAsync().ConfigureAwait(false); return accounts.Any(account => account.Id != excludedAccountId && string.Equals(account.Address?.Trim(), normalizedAddress, StringComparison.OrdinalIgnoreCase)); } public async Task CreateRootAliasAsync(Guid accountId, string address) { if (string.IsNullOrWhiteSpace(address)) return; var rootAlias = new MailAccountAlias() { AccountId = accountId, AliasAddress = address, IsPrimary = true, IsRootAlias = true, IsVerified = true, ReplyToAddress = address, Id = Guid.NewGuid(), Source = AliasSource.Manual, SendCapability = AliasSendCapability.Confirmed }; await Connection.InsertAsync(rootAlias, typeof(MailAccountAlias)).ConfigureAwait(false); Log.Information("Created root alias for the account {AccountId}", accountId); } public async Task> GetAccountAliasesAsync(Guid accountId) { return await Connection.QueryAsync( "SELECT * FROM MailAccountAlias WHERE AccountId = ? ORDER BY IsRootAlias DESC, IsPrimary DESC, AliasAddress ASC", accountId).ConfigureAwait(false); } private Task GetMergedInboxInformationAsync(Guid mergedInboxId) => Connection.Table().FirstOrDefaultAsync(a => a.Id == mergedInboxId); public async Task DeleteAccountMailCacheAsync(Guid accountId, AccountCacheResetReason accountCacheResetReason) { await Connection.ExecuteAsync( "DELETE FROM MailCopy WHERE Id IN (SELECT Id FROM MailCopy WHERE FolderId IN (SELECT Id FROM MailItemFolder WHERE MailAccountId = ?))", accountId); WeakReferenceMessenger.Default.Send(new AccountCacheResetMessage(accountId, accountCacheResetReason)); } public async Task DeleteAccountAsync(MailAccount account) { // Collect calendar entities before deletion so we can notify UI subscribers. var accountCalendars = await Connection.Table() .Where(a => a.AccountId == account.Id) .ToListAsync() .ConfigureAwait(false); var deletedCalendarItems = new List(); foreach (var accountCalendar in accountCalendars) { var calendarItems = await Connection.Table() .Where(a => a.CalendarId == accountCalendar.Id) .ToListAsync() .ConfigureAwait(false); deletedCalendarItems.AddRange(calendarItems); } await DeleteAccountMailCacheAsync(account.Id, AccountCacheResetReason.AccountRemoval); // Delete calendar metadata and related records for this account. foreach (var calendarItem in deletedCalendarItems) { await Connection.Table().DeleteAsync(a => a.CalendarItemId == calendarItem.Id).ConfigureAwait(false); await Connection.Table().DeleteAsync(a => a.CalendarItemId == calendarItem.Id).ConfigureAwait(false); await Connection.Table().DeleteAsync(a => a.CalendarItemId == calendarItem.Id).ConfigureAwait(false); } foreach (var accountCalendar in accountCalendars) { await Connection.Table().DeleteAsync(a => a.CalendarId == accountCalendar.Id).ConfigureAwait(false); } await Connection.Table().DeleteAsync(a => a.AccountId == account.Id).ConfigureAwait(false); 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. if (account.MergedInboxId != null) { var mergedInboxAccountCount = await Connection.Table().Where(a => a.MergedInboxId == account.MergedInboxId.Value).CountAsync(); // There will be only one account in the merged inbox. Remove the link for the other account as well. if (mergedInboxAccountCount == 2) { await Connection.ExecuteAsync( "UPDATE MailAccount SET MergedInboxId = NULL WHERE MergedInboxId = ?", account.MergedInboxId.Value).ConfigureAwait(false); } } if (account.ProviderType == MailProviderType.IMAP4) await Connection.Table().DeleteAsync(a => a.AccountId == account.Id); if (account.Preferences != null) await Connection.DeleteAsync(account.Preferences.Id); await Connection.DeleteAsync(account.Id); await _mimeFileService.DeleteUserMimeCacheAsync(account.Id).ConfigureAwait(false); // Clear out or set up a new startup entity id. // Next account after the deleted one will be the startup account. if (_preferencesService.StartupEntityId == account.Id || _preferencesService.StartupEntityId == account.MergedInboxId) { var firstNonStartupAccount = await Connection.Table().FirstOrDefaultAsync(a => a.Id != account.Id); if (firstNonStartupAccount != null) { _preferencesService.StartupEntityId = firstNonStartupAccount.Id; } else { _preferencesService.StartupEntityId = null; } } foreach (var calendarItem in deletedCalendarItems) { WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(calendarItem, EntityUpdateSource.Server)); } foreach (var accountCalendar in accountCalendars) { WeakReferenceMessenger.Default.Send(new CalendarListDeleted(accountCalendar)); } 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; if (string.IsNullOrEmpty(account.Address)) { account.Address = profileInformation.AccountAddress; } // Forcefully add or update a contact data with the provided information. var existingContact = await Connection.Table() .FirstOrDefaultAsync(a => a.Address == account.Address) .ConfigureAwait(false); var contactPictureFileId = await SaveProfilePictureAsync( account.Base64ProfilePictureData, existingContact?.ContactPictureFileId).ConfigureAwait(false); var accountContact = new AccountContact() { Address = account.Address, Name = account.SenderName, ContactPictureFileId = contactPictureFileId, IsRootContact = true }; await Connection.InsertOrReplaceAsync(accountContact, typeof(AccountContact)).ConfigureAwait(false); await UpdateAccountAsync(account).ConfigureAwait(false); } } private async Task SaveProfilePictureAsync(string base64ProfilePictureData, Guid? existingFileId) { if (string.IsNullOrWhiteSpace(base64ProfilePictureData)) { if (existingFileId.HasValue) await _contactPictureFileService.DeleteContactPictureAsync(existingFileId.Value).ConfigureAwait(false); return null; } byte[] bytes; try { bytes = Convert.FromBase64String(base64ProfilePictureData); } catch (FormatException ex) { _logger.Warning(ex, "Failed to decode account profile picture for contact migration."); return existingFileId; } var newFileId = await _contactPictureFileService.SaveContactPictureAsync(bytes).ConfigureAwait(false); if (existingFileId.HasValue) await _contactPictureFileService.DeleteContactPictureAsync(existingFileId.Value).ConfigureAwait(false); return newFileId; } public async Task GetAccountAsync(Guid accountId) { var account = await Connection.Table().FirstOrDefaultAsync(a => a.Id == accountId); if (account == null) { _logger.Error("Could not find account with id {AccountId}", accountId); } else { if (account.ProviderType == MailProviderType.IMAP4) account.ServerInformation = await GetAccountCustomServerInformationAsync(account.Id); account.Preferences = await GetAccountPreferencesAsync(account.Id); return account; } return null; } public Task GetAccountCustomServerInformationAsync(Guid accountId) => Connection.Table().FirstOrDefaultAsync(a => a.AccountId == accountId); public async Task UpdateAccountAsync(MailAccount account) { await Connection.UpdateAsync(account.Preferences, typeof(MailAccountPreferences)).ConfigureAwait(false); await Connection.UpdateAsync(account, typeof(MailAccount)).ConfigureAwait(false); ReportUIChange(new AccountUpdatedMessage(account)); } public async Task UpdateAccountCustomServerInformationAsync(CustomServerInformation customServerInformation) { await Connection.InsertOrReplaceAsync(customServerInformation, typeof(CustomServerInformation)).ConfigureAwait(false); } public async Task UpdateAccountAliasesAsync(Guid accountId, List aliases) { // Delete existing ones. await Connection.Table().DeleteAsync(a => a.AccountId == accountId).ConfigureAwait(false); // Insert new ones. foreach (var alias in aliases) { await Connection.InsertAsync(alias, typeof(MailAccountAlias)).ConfigureAwait(false); } } public async Task UpdateRemoteAliasInformationAsync(MailAccount account, List remoteAccountAliases) { var localAliases = await GetAccountAliasesAsync(account.Id).ConfigureAwait(false); var normalizedRemoteAliases = remoteAccountAliases ?? []; foreach (var remoteAlias in normalizedRemoteAliases) { if (string.IsNullOrWhiteSpace(remoteAlias?.AliasAddress)) continue; var existingAlias = localAliases.Find(a => a.AccountId == account.Id && a.AliasAddress.Equals(remoteAlias.AliasAddress, StringComparison.OrdinalIgnoreCase)); 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, AliasSenderName = remoteAlias.AliasSenderName, Source = remoteAlias.Source, SendCapability = remoteAlias.SendCapability }; await Connection.InsertAsync(newAlias, typeof(MailAccountAlias)); localAliases.Add(newAlias); } else { // Update existing alias. existingAlias.IsPrimary = remoteAlias.IsPrimary; existingAlias.IsVerified = remoteAlias.IsVerified; existingAlias.ReplyToAddress = remoteAlias.ReplyToAddress; existingAlias.AliasSenderName = remoteAlias.AliasSenderName; existingAlias.Source = remoteAlias.Source; existingAlias.SendCapability = remoteAlias.SendCapability; await Connection.UpdateAsync(existingAlias, typeof(MailAccountAlias)); } } if (localAliases.Count == 0 && !string.IsNullOrWhiteSpace(account.Address)) { var fallbackAddress = account.Address.Trim(); var fallbackAlias = new MailAccountAlias() { AccountId = account.Id, AliasAddress = fallbackAddress, IsPrimary = true, IsRootAlias = true, IsVerified = true, ReplyToAddress = fallbackAddress, Id = Guid.NewGuid(), Source = AliasSource.ProviderDiscovered, SendCapability = AliasSendCapability.Confirmed }; await Connection.InsertAsync(fallbackAlias, typeof(MailAccountAlias)).ConfigureAwait(false); localAliases.Add(fallbackAlias); } // 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, typeof(MailAccountAlias)).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, typeof(MailAccountAlias)).ConfigureAwait(false); } } public async Task UpdateAliasSendCapabilityAsync(Guid accountId, string aliasAddress, AliasSendCapability capability) { if (string.IsNullOrWhiteSpace(aliasAddress)) return; var aliases = await GetAccountAliasesAsync(accountId).ConfigureAwait(false); var alias = aliases.FirstOrDefault(a => a.AliasAddress.Equals(aliasAddress, StringComparison.OrdinalIgnoreCase)); if (alias == null) return; alias.SendCapability = capability; await Connection.UpdateAsync(alias, typeof(MailAccountAlias)).ConfigureAwait(false); } public async Task DeleteAccountAliasAsync(Guid aliasId) { // Create query to delete alias. await Connection.ExecuteAsync("DELETE FROM MailAccountAlias WHERE Id = ?", aliasId).ConfigureAwait(false); } public async Task CreateAccountAsync(MailAccount account, CustomServerInformation customServerInformation) { Guard.IsNotNull(account); if (await AccountNameExistsAsync(account.Name).ConfigureAwait(false)) throw new InvalidOperationException(Translator.DialogMessage_AccountNameExistsMessage); if (await AccountAddressExistsAsync(account.Address).ConfigureAwait(false)) throw new InvalidOperationException(Translator.DialogMessage_AccountAddressExistsMessage); if (!account.CreatedAt.HasValue) { account.CreatedAt = DateTime.UtcNow; } var accountCount = await Connection.Table().CountAsync(); // If there are no accounts before this one, set it as startup account. if (accountCount == 0) { _preferencesService.StartupEntityId = account.Id; } else { // Set the order of the account. // This can be changed by the user later in manage accounts page. account.Order = accountCount; } await Connection.InsertAsync(account, typeof(MailAccount)); var preferences = new MailAccountPreferences() { Id = Guid.NewGuid(), AccountId = account.Id, IsNotificationsEnabled = true, ShouldAppendMessagesToSentFolder = false }; // iCloud does not appends sent messages to sent folder automatically. if (account.SpecialImapProvider == SpecialImapProvider.iCloud || account.SpecialImapProvider == SpecialImapProvider.Yahoo) { preferences.ShouldAppendMessagesToSentFolder = true; } account.Preferences = preferences; // Outlook & Office 365 supports Focused inbox. Enabled by default. bool isMicrosoftProvider = account.ProviderType == MailProviderType.Outlook; // TODO: This should come from account settings API. // Wino doesn't have MailboxSettings yet. if (isMicrosoftProvider) account.Preferences.IsFocusedInboxEnabled = true; // Setup default signature. var defaultSignature = await _signatureService.CreateDefaultSignatureAsync(account.Id); account.Preferences.SignatureIdForNewMessages = defaultSignature.Id; account.Preferences.SignatureIdForFollowingMessages = defaultSignature.Id; account.Preferences.IsSignatureEnabled = true; await Connection.InsertAsync(preferences, typeof(MailAccountPreferences)); if (customServerInformation != null) await Connection.InsertAsync(customServerInformation, typeof(CustomServerInformation)); if (account.ProviderType == MailProviderType.IMAP4 && customServerInformation?.CalendarSupportMode == ImapCalendarSupportMode.LocalOnly) { await EnsureDefaultLocalCalendarForImapAsync(account.Id).ConfigureAwait(false); } } private async Task EnsureDefaultLocalCalendarForImapAsync(Guid accountId) { var existingCalendarCount = await Connection.Table() .Where(a => a.AccountId == accountId) .CountAsync() .ConfigureAwait(false); if (existingCalendarCount > 0) return; var localCalendar = new AccountCalendar { Id = Guid.NewGuid(), AccountId = accountId, Name = Translator.AccountDetailsPage_TabCalendar, IsPrimary = true, IsSynchronizationEnabled = true, IsExtended = true, RemoteCalendarId = string.Empty, TimeZone = string.Empty, BackgroundColorHex = await GetNextDistinctCalendarColorAsync().ConfigureAwait(false) }; localCalendar.TextColorHex = GetReadableTextColorHex(localCalendar.BackgroundColorHex); await Connection.InsertAsync(localCalendar, typeof(AccountCalendar)).ConfigureAwait(false); } private async Task GetNextDistinctCalendarColorAsync() { var usedColors = await Connection.Table() .ToListAsync() .ConfigureAwait(false); return CalendarColorPalette.GetDistinctColor(usedColors.Select(a => a.BackgroundColorHex)); } private static string GetReadableTextColorHex(string backgroundColorHex) { if (!TryParseHexColor(backgroundColorHex, out var red, out var green, out var blue)) return "#FFFFFF"; var luminance = ((0.299 * red) + (0.587 * green) + (0.114 * blue)) / 255d; return luminance > 0.6 ? "#111111" : "#FFFFFF"; } private static bool TryParseHexColor(string value, out int red, out int green, out int blue) { red = 255; green = 255; blue = 255; if (string.IsNullOrWhiteSpace(value)) return false; var color = value.Trim(); if (color.StartsWith('#')) { color = color[1..]; } if (color.Length != 6 || !int.TryParse(color, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _)) { return false; } red = Convert.ToInt32(color.Substring(0, 2), 16); green = Convert.ToInt32(color.Substring(2, 2), 16); blue = Convert.ToInt32(color.Substring(4, 2), 16); return true; } public async Task UpdateAccountOrdersAsync(Dictionary accountIdOrderPair) { foreach (var pair in accountIdOrderPair) { var account = await GetAccountAsync(pair.Key); if (account == null) { _logger.Information("Could not find account with id {Key} for reordering. It may be a linked account.", pair.Key); continue; } account.Order = pair.Value; await Connection.UpdateAsync(account, typeof(MailAccount)); } Messenger.Send(new AccountMenuItemsReordered(accountIdOrderPair)); } public async Task GetPrimaryAccountAliasAsync(Guid accountId) { var aliases = await GetAccountAliasesAsync(accountId); if (aliases == null || aliases.Count == 0) return null; return aliases.FirstOrDefault(a => a.IsPrimary) ?? aliases.First(); } public async Task IsAccountFocusedEnabledAsync(Guid accountId) { var account = await GetAccountAsync(accountId); return account.Preferences.IsFocusedInboxEnabled.GetValueOrDefault(); } public async Task IsNotificationsEnabled(Guid accountId) { var account = await GetAccountAsync(accountId); return account?.Preferences?.IsNotificationsEnabled ?? false; } public async Task UpdateLastFolderStructureSyncDateAsync(Guid accountId) { var account = await GetAccountAsync(accountId); if (account == null) return; account.LastFolderStructureSyncDate = DateTime.UtcNow; await Connection.UpdateAsync(account, typeof(MailAccount)).ConfigureAwait(false); } public async Task ShouldSyncFolderStructureAsync(Guid accountId, TimeSpan syncInterval) { var account = await GetAccountAsync(accountId); if (account == null) return true; if (!account.LastFolderStructureSyncDate.HasValue) return true; return DateTime.UtcNow - account.LastFolderStructureSyncDate.Value > syncInterval; } }