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 ;
using Wino.Core.Domain.Enums ;
using Wino.Core.Domain.Interfaces ;
using Wino.Core.Extensions ;
2024-08-05 00:36:26 +02:00
using Wino.Messaging.Client.Accounts ;
using Wino.Messaging.UI ;
2024-04-18 01:44:37 +02:00
namespace Wino.Core.Services
{
public class AccountService : BaseDatabaseService , IAccountService
{
public IAuthenticator ExternalAuthenticationAuthenticator { get ; set ; }
private readonly IAuthenticationProvider _authenticationProvider ;
private readonly ISignatureService _signatureService ;
private readonly IPreferencesService _preferencesService ;
private readonly ILogger _logger = Log . ForContext < AccountService > ( ) ;
public AccountService ( IDatabaseService databaseService ,
IAuthenticationProvider authenticationProvider ,
ISignatureService signatureService ,
IPreferencesService preferencesService ) : base ( databaseService )
{
_authenticationProvider = authenticationProvider ;
_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 ) ;
// This will re-generate token.
var token = await authenticator . GenerateTokenAsync ( account , true ) ;
Guard . IsNotNull ( token ) ;
}
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 ) ;
2024-08-15 16:02:02 +02:00
// Load aliases
account . Aliases = await GetAccountAliases ( account . Id , account . Address ) ;
2024-04-18 01:44:37 +02:00
account . Preferences = await GetAccountPreferencesAsync ( account . Id ) ;
}
return accounts ;
}
2024-08-15 16:02:02 +02:00
private async Task < List < MailAccountAlias > > GetAccountAliases ( Guid accountId , string primaryAccountAddress )
{
// 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.
2024-08-16 01:29:31 +02:00
var aliases = await Connection
. Table < MailAccountAlias > ( )
. Where ( a = > a . AccountId = = accountId )
. ToListAsync ( )
. ConfigureAwait ( false ) ;
2024-08-15 16:02:02 +02:00
if ( ! aliases . Any ( ) )
{
var primaryAccountAlias = new MailAccountAlias ( )
{
Id = Guid . NewGuid ( ) ,
AccountId = accountId ,
IsPrimary = true ,
AliasAddress = primaryAccountAddress ,
2024-08-15 16:11:12 +02:00
ReplyToAddress = primaryAccountAddress ,
2024-08-15 16:02:02 +02:00
IsVerified = true ,
} ;
await Connection . InsertAsync ( primaryAccountAlias ) . ConfigureAwait ( false ) ;
aliases . Add ( primaryAccountAlias ) ;
}
return aliases ;
}
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.
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 < TokenInformation > ( ) . Where ( a = > a . AccountId = = account . Id ) . DeleteAsync ( ) ;
await Connection . Table < MailItemFolder > ( ) . DeleteAsync ( a = > a . MailAccountId = = account . Id ) ;
2024-06-13 00:51:59 +02:00
await Connection . Table < AccountSignature > ( ) . DeleteAsync ( a = > a . MailAccountId = = 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 < 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 )
{
2024-08-16 01:29:31 +02:00
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 ) ) ;
}
2024-08-16 01:29:31 +02:00
public async Task UpdateAccountAliases ( 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-04-18 01:44:37 +02:00
public async Task CreateAccountAsync ( MailAccount account , TokenInformation tokenInformation , CustomServerInformation customServerInformation )
{
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 ;
2024-06-13 00:51:59 +02:00
// Setup default signature.
2024-04-18 01:44:37 +02:00
var defaultSignature = await _signatureService . CreateDefaultSignatureAsync ( account . Id ) ;
2024-06-13 00:51:59 +02:00
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 ) ;
2024-08-05 00:36:26 +02:00
// Outlook token cache is managed by MSAL.
// Don't save it to database.
if ( tokenInformation ! = null & & account . ProviderType ! = MailProviderType . Outlook )
2024-04-18 01:44:37 +02:00
await Connection . InsertAsync ( tokenInformation ) ;
}
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 )
& & ulong . TryParse ( currentIdentifier , out ulong currentIdentifierValue )
& & ulong . TryParse ( newIdentifier , out ulong newIdentifierValue )
& & newIdentifierValue > currentIdentifierValue ) ) : true ;
if ( shouldUpdateIdentifier )
{
_logger . Debug ( "Updating synchronization identifier for {Name}. From: {SynchronizationDeltaIdentifier} To: {NewIdentifier}" , account . Name , account . SynchronizationDeltaIdentifier , newIdentifier ) ;
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 )
{
2024-06-09 02:37:30 +02:00
_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 ) ;
}
2024-06-09 02:37:30 +02:00
Messenger . Send ( new AccountMenuItemsReordered ( accountIdOrderPair ) ) ;
2024-05-30 02:34:54 +02:00
}
2024-04-18 01:44:37 +02:00
}
}