Files
Wino-Mail/Wino.Services/AccountService.cs

589 lines
22 KiB
C#
Raw Normal View History

2024-04-18 01:44:37 +02:00
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Diagnostics;
using CommunityToolkit.Mvvm.Messaging;
using Serilog;
using SqlKata;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
Full trust Wino Server implementation. (#295) * Separation of messages. Introducing Wino.Messages library. * Wino.Server and Wino.Packaging projects. Enabling full trust for UWP and app service connection manager basics. * Remove debug code. * Enable generating assembly info to deal with unsupported os platform warnings. * Fix server-client connection. * UIMessage communication. Single instancing for server and re-connection mechanism on suspension. * Removed IWinoSynchronizerFactory from UWP project. * Removal of background task service from core. * Delegating changes to UI and triggering new background synchronization. * Fix build error. * Moved core lib messages to Messaging project. * Better client-server communication. Handling of requests in the server. New synchronizer factory in the server. * WAM broker and MSAL token caching for OutlookAuthenticator. Handling account creation for Outlook. * WinoServerResponse basics. * Delegating protocol activation for Gmail authenticator. * Adding margin to searchbox to match action bar width. * Move libraries into lib folder. * Storing base64 encoded mime on draft creation instead of MimeMessage object. Fixes serialization/deserialization issue with S.T.Json * Scrollbar adjustments * WınoExpander for thread expander layout ıssue. * Handling synchronizer state changes. * Double init on background activation. * FIxing packaging issues and new Wino Mail launcher protocol for activation from full thrust process. * Remove debug deserialization. * Remove debug code. * Making sure the server connection is established when the app is launched. * Thrust -> Trust string replacement... * Rename package to Wino Mail * Enable translated values in the server. * Fixed an issue where toast activation can't find the clicked mail after the folder is initialized. * Revert debug code. * Change server background sync to every 3 minute and Inbox only synchronization. * Revert google auth changes. * App preferences page. * Changing tray icon visibility on preference change. * Start the server with invisible tray icon if set to invisible. * Reconnect button on the title bar. * Handling of toast actions. * Enable x86 build for server during packaging. * Get rid of old background tasks and v180 migration. * Terminate client when Exit clicked in server. * Introducing SynchronizationSource to prevent notifying UI after server tick synchronization. * Remove confirmAppClose restricted capability and unused debug code in manifest. * Closing the reconnect info popup when reconnect is clicked. * Custom RetryHandler for OutlookSynchronizer and separating client/server logs. * Running server on Windows startup. * Fix startup exe. * Fix for expander list view item paddings. * Force full sync on app launch instead of Inbox. * Fix draft creation. * Fix an issue with custom folder sync logic. * Reporting back account sync progress from server. * Fix sending drafts and missing notifications for imap. * Changing imap folder sync requirements. * Retain file count is set to 3. * Disabled swipe gestures temporarily due to native crash with SwipeControl * Save all attachments implementation. * Localization for save all attachments button. * Fix logging dates for logs. * Fixing ARM64 build. * Add ARM64 build config to packaging project. * Comment out OutOfProcPDB for ARM64. * Hnadling GONE response for Outlook folder synchronization.
2024-08-05 00:36:26 +02:00
using Wino.Messaging.Client.Accounts;
using Wino.Messaging.UI;
using Wino.Services.Extensions;
2024-04-18 01:44:37 +02:00
namespace Wino.Services
2024-04-18 01:44:37 +02:00
{
public class AccountService : BaseDatabaseService, IAccountService
{
public IAuthenticator ExternalAuthenticationAuthenticator { get; set; }
private readonly ISignatureService _signatureService;
private readonly IPreferencesService _preferencesService;
private readonly ILogger _logger = Log.ForContext<AccountService>();
public AccountService(IDatabaseService databaseService,
ISignatureService signatureService,
IPreferencesService preferencesService) : base(databaseService)
{
_signatureService = signatureService;
_preferencesService = preferencesService;
}
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<Guid> 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 query = new Query("MailAccount")
.WhereIn("Id", linkedAccountIds)
.AsUpdate(new
{
MergedInboxId = mergedInboxId
});
await Connection.ExecuteAsync(query.GetRawQuery());
WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested());
}
public async Task UnlinkMergedInboxAsync(Guid mergedInboxId)
{
var mergedInbox = await Connection.Table<MergedInbox>().FirstOrDefaultAsync(a => a.Id == mergedInboxId).ConfigureAwait(false);
if (mergedInbox == null)
{
_logger.Warning("Could not find merged inbox with id {MergedInboxId}", mergedInboxId);
return;
}
var query = new Query("MailAccount")
.Where("MergedInboxId", mergedInboxId)
.AsUpdate(new
{
MergedInboxId = (Guid?)null
});
await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
await Connection.DeleteAsync(mergedInbox).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<MailAccount>().FirstOrDefaultAsync();
if (firstAccount != null)
{
_preferencesService.StartupEntityId = firstAccount.Id;
}
else
{
_preferencesService.StartupEntityId = null;
}
}
WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested());
}
public async Task CreateMergeAccountsAsync(MergedInbox mergedInbox, IEnumerable<MailAccount> accountsToMerge)
{
if (mergedInbox == null) return;
// 0. Give the merged inbox a new Guid.
mergedInbox.Id = Guid.NewGuid();
var accountFolderDictionary = new Dictionary<MailAccount, List<MailItemFolder>>();
// 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<MailItemFolder>();
var folders = await Connection.Table<MailItemFolder>().Where(a => a.MailAccountId == account.Id).ToListAsync();
foreach (var folder in folders)
{
accountFolderList.Add(folder);
folder.IsSticky = false;
await Connection.UpdateAsync(folder);
}
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);
}
}
}
}
// 3. Insert merged inbox and assign accounts.
await Connection.InsertAsync(mergedInbox);
foreach (var account in accountsToMerge)
{
account.MergedInboxId = mergedInbox.Id;
await Connection.UpdateAsync(account);
}
WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested());
}
public async Task RenameMergedAccountAsync(Guid mergedInboxId, string newName)
{
var query = new Query("MergedInbox")
.Where("Id", mergedInboxId)
.AsUpdate(new
{
Name = newName
});
await Connection.ExecuteAsync(query.GetRawQuery());
ReportUIChange(new MergedInboxRenamed(mergedInboxId, newName));
}
public async Task FixTokenIssuesAsync(Guid accountId)
{
var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);
if (account == null) return;
//var authenticator = _authenticationProvider.GetAuthenticator(account.ProviderType);
2024-04-18 01:44:37 +02:00
//// This will re-generate token.
//var token = await authenticator.GenerateTokenInformationAsync(account);
2024-04-18 01:44:37 +02:00
// TODO: Rest?
// Guard.IsNotNull(token);
2024-04-18 01:44:37 +02:00
}
private Task<MailAccountPreferences> GetAccountPreferencesAsync(Guid accountId)
=> Connection.Table<MailAccountPreferences>().FirstOrDefaultAsync(a => a.AccountId == accountId);
public async Task<List<MailAccount>> GetAccountsAsync()
{
2024-05-30 02:34:54 +02:00
var accounts = await Connection.Table<MailAccount>().OrderBy(a => a.Order).ToListAsync();
2024-04-18 01:44:37 +02:00
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 CreateRootAliasAsync(Guid accountId, string address)
2024-08-15 16:02:02 +02:00
{
var rootAlias = new MailAccountAlias()
{
AccountId = accountId,
AliasAddress = address,
IsPrimary = true,
IsRootAlias = true,
IsVerified = true,
ReplyToAddress = address,
Id = Guid.NewGuid()
};
2024-08-15 16:02:02 +02:00
await Connection.InsertAsync(rootAlias).ConfigureAwait(false);
2024-08-15 16:02:02 +02:00
Log.Information("Created root alias for the account {AccountId}", accountId);
}
public async Task<List<MailAccountAlias>> GetAccountAliasesAsync(Guid accountId)
{
var query = new Query(nameof(MailAccountAlias))
.Where(nameof(MailAccountAlias.AccountId), accountId)
.OrderByDesc(nameof(MailAccountAlias.IsRootAlias));
2024-08-15 16:02:02 +02:00
return await Connection.QueryAsync<MailAccountAlias>(query.GetRawQuery()).ConfigureAwait(false);
2024-08-15 16:02:02 +02:00
}
2024-04-18 01:44:37 +02:00
private Task<MergedInbox> GetMergedInboxInformationAsync(Guid mergedInboxId)
=> Connection.Table<MergedInbox>().FirstOrDefaultAsync(a => a.Id == mergedInboxId);
public async Task DeleteAccountAsync(MailAccount account)
{
// TODO: Delete mime messages and attachments.
// TODO: Delete token cache by underlying provider.
2024-04-18 01:44:37 +02:00
await Connection.ExecuteAsync("DELETE FROM MailCopy WHERE Id IN(SELECT Id FROM MailCopy WHERE FolderId IN (SELECT Id from MailItemFolder WHERE MailAccountId == ?))", account.Id);
await Connection.Table<MailItemFolder>().DeleteAsync(a => a.MailAccountId == account.Id);
await Connection.Table<AccountSignature>().DeleteAsync(a => a.MailAccountId == account.Id);
await Connection.Table<MailAccountAlias>().DeleteAsync(a => a.AccountId == account.Id);
2024-04-18 01:44:37 +02:00
// 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<MailAccount>().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)
{
var query = new Query("MailAccount")
.Where("MergedInboxId", account.MergedInboxId.Value)
.AsUpdate(new
{
MergedInboxId = (Guid?)null
});
await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
}
}
if (account.ProviderType == MailProviderType.IMAP4)
await Connection.Table<CustomServerInformation>().DeleteAsync(a => a.AccountId == account.Id);
if (account.Preferences != null)
await Connection.DeleteAsync(account.Preferences);
await Connection.DeleteAsync(account);
// 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<MailAccount>().FirstOrDefaultAsync(a => a.Id != account.Id);
if (firstNonStartupAccount != null)
{
_preferencesService.StartupEntityId = firstNonStartupAccount.Id;
}
else
{
_preferencesService.StartupEntityId = null;
}
}
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;
}
2024-08-23 02:07:25 +02:00
// Forcefully add or update a contact data with the provided information.
var accountContact = new AccountContact()
{
Address = account.Address,
Name = account.SenderName,
2024-08-24 00:14:32 +02:00
Base64ContactPicture = account.Base64ProfilePictureData,
IsRootContact = true
2024-08-23 02:07:25 +02:00
};
await Connection.InsertOrReplaceAsync(accountContact).ConfigureAwait(false);
await UpdateAccountAsync(account).ConfigureAwait(false);
}
}
2024-04-18 01:44:37 +02:00
public async Task<MailAccount> GetAccountAsync(Guid accountId)
{
var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);
2024-05-30 02:34:54 +02:00
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);
2024-04-18 01:44:37 +02:00
2024-05-30 02:34:54 +02:00
account.Preferences = await GetAccountPreferencesAsync(account.Id);
2024-04-18 01:44:37 +02:00
2024-05-30 02:34:54 +02:00
return account;
}
2024-04-18 01:44:37 +02:00
2024-05-30 02:34:54 +02:00
return null;
2024-04-18 01:44:37 +02:00
}
public Task<CustomServerInformation> GetAccountCustomServerInformationAsync(Guid accountId)
=> Connection.Table<CustomServerInformation>().FirstOrDefaultAsync(a => a.AccountId == accountId);
public async Task UpdateAccountAsync(MailAccount account)
{
await Connection.UpdateAsync(account.Preferences).ConfigureAwait(false);
await Connection.UpdateAsync(account).ConfigureAwait(false);
2024-04-18 01:44:37 +02:00
ReportUIChange(new AccountUpdatedMessage(account));
}
public async Task UpdateAccountAliasesAsync(Guid accountId, List<MailAccountAlias> aliases)
{
// Delete existing ones.
await Connection.Table<MailAccountAlias>().DeleteAsync(a => a.AccountId == accountId).ConfigureAwait(false);
// Insert new ones.
foreach (var alias in aliases)
{
await Connection.InsertAsync(alias).ConfigureAwait(false);
}
}
2024-08-17 19:54:52 +02:00
public async Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases)
{
var localAliases = await GetAccountAliasesAsync(account.Id).ConfigureAwait(false);
var rootAlias = localAliases.Find(a => a.IsRootAlias);
foreach (var remoteAlias in remoteAccountAliases)
{
var existingAlias = localAliases.Find(a => a.AccountId == account.Id && a.AliasAddress == remoteAlias.AliasAddress);
if (existingAlias == null)
{
// Create new alias.
var newAlias = new MailAccountAlias()
{
AccountId = account.Id,
AliasAddress = remoteAlias.AliasAddress,
IsPrimary = remoteAlias.IsPrimary,
IsVerified = remoteAlias.IsVerified,
ReplyToAddress = remoteAlias.ReplyToAddress,
Id = Guid.NewGuid(),
IsRootAlias = remoteAlias.IsRootAlias,
AliasSenderName = remoteAlias.AliasSenderName
2024-08-17 19:54:52 +02:00
};
await Connection.InsertAsync(newAlias);
localAliases.Add(newAlias);
}
else
{
// Update existing alias.
existingAlias.IsPrimary = remoteAlias.IsPrimary;
existingAlias.IsVerified = remoteAlias.IsVerified;
existingAlias.ReplyToAddress = remoteAlias.ReplyToAddress;
existingAlias.AliasSenderName = remoteAlias.AliasSenderName;
2024-08-17 19:54:52 +02:00
await Connection.UpdateAsync(existingAlias);
}
}
// Make sure there is only 1 root alias and 1 primary alias selected.
bool shouldUpdatePrimary = localAliases.Count(a => a.IsPrimary) != 1;
bool shouldUpdateRoot = localAliases.Count(a => a.IsRootAlias) != 1;
if (shouldUpdatePrimary)
{
localAliases.ForEach(a => a.IsPrimary = false);
var idealPrimaryAlias = localAliases.Find(a => a.AliasAddress == account.Address) ?? localAliases.First();
idealPrimaryAlias.IsPrimary = true;
await Connection.UpdateAsync(idealPrimaryAlias).ConfigureAwait(false);
}
if (shouldUpdateRoot)
{
localAliases.ForEach(a => a.IsRootAlias = false);
var idealRootAlias = localAliases.Find(a => a.AliasAddress == account.Address) ?? localAliases.First();
idealRootAlias.IsRootAlias = true;
await Connection.UpdateAsync(idealRootAlias).ConfigureAwait(false);
}
}
public async Task DeleteAccountAliasAsync(Guid aliasId)
{
// Create query to delete alias.
var query = new Query("MailAccountAlias")
.Where("Id", aliasId)
.AsDelete();
await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
}
public async Task CreateAccountAsync(MailAccount account, CustomServerInformation customServerInformation)
2024-04-18 01:44:37 +02:00
{
Guard.IsNotNull(account);
var accountCount = await Connection.Table<MailAccount>().CountAsync();
// If there are no accounts before this one, set it as startup account.
if (accountCount == 0)
{
_preferencesService.StartupEntityId = account.Id;
}
2024-05-30 02:34:54 +02:00
else
{
// Set the order of the account.
// This can be changed by the user later in manage accounts page.
account.Order = accountCount;
}
2024-04-18 01:44:37 +02:00
await Connection.InsertAsync(account);
var preferences = new MailAccountPreferences()
{
Id = Guid.NewGuid(),
AccountId = account.Id,
IsNotificationsEnabled = true,
ShouldAppendMessagesToSentFolder = false
};
account.Preferences = preferences;
// Outlook & Office 365 supports Focused inbox. Enabled by default.
bool isMicrosoftProvider = account.ProviderType == MailProviderType.Outlook || account.ProviderType == MailProviderType.Office365;
2024-05-30 02:34:54 +02:00
// TODO: This should come from account settings API.
// Wino doesn't have MailboxSettings yet.
2024-04-18 01:44:37 +02:00
if (isMicrosoftProvider)
account.Preferences.IsFocusedInboxEnabled = true;
// Setup default signature.
2024-04-18 01:44:37 +02:00
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);
2024-04-18 01:44:37 +02:00
if (customServerInformation != null)
await Connection.InsertAsync(customServerInformation);
}
public async Task<string> UpdateSynchronizationIdentifierAsync(Guid accountId, string newIdentifier)
{
var account = await GetAccountAsync(accountId);
if (account == null)
{
_logger.Error("Could not find account with id {AccountId}", accountId);
return string.Empty;
}
var currentIdentifier = account.SynchronizationDeltaIdentifier;
bool shouldUpdateIdentifier = account.ProviderType == MailProviderType.Gmail ?
string.IsNullOrEmpty(currentIdentifier) ? true : !string.IsNullOrEmpty(currentIdentifier)
2024-04-18 01:44:37 +02:00
&& ulong.TryParse(currentIdentifier, out ulong currentIdentifierValue)
&& ulong.TryParse(newIdentifier, out ulong newIdentifierValue)
&& newIdentifierValue > currentIdentifierValue : true;
2024-04-18 01:44:37 +02:00
if (shouldUpdateIdentifier)
{
account.SynchronizationDeltaIdentifier = newIdentifier;
await UpdateAccountAsync(account);
}
return account.SynchronizationDeltaIdentifier;
}
2024-05-30 02:34:54 +02:00
public async Task UpdateAccountOrdersAsync(Dictionary<Guid, int> 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);
2024-05-30 02:34:54 +02:00
continue;
}
account.Order = pair.Value;
2024-04-18 01:44:37 +02:00
2024-05-30 02:34:54 +02:00
await Connection.UpdateAsync(account);
}
Messenger.Send(new AccountMenuItemsReordered(accountIdOrderPair));
2024-05-30 02:34:54 +02:00
}
public async Task<MailAccountAlias> GetPrimaryAccountAliasAsync(Guid accountId)
{
var aliases = await GetAccountAliasesAsync(accountId);
if (aliases == null || aliases.Count == 0) return null;
return aliases.FirstOrDefault(a => a.IsPrimary) ?? aliases.First();
}
public async Task<bool> IsAccountFocusedEnabledAsync(Guid accountId)
{
var account = await GetAccountAsync(accountId);
return account.Preferences.IsFocusedInboxEnabled.GetValueOrDefault();
}
2024-04-18 01:44:37 +02:00
}
}