Managing account aliases and profile synchronization for outlook and gmail.

This commit is contained in:
Burak Kaan Köse
2024-08-17 03:43:37 +02:00
parent f1154058ba
commit abff850427
46 changed files with 949 additions and 272 deletions

View File

@@ -205,21 +205,22 @@ namespace Wino.Core.Extensions
};
}
public static List<MailAccountAlias> GetMailAliases(this ListSendAsResponse response, MailAccount currentAccount)
public static List<MailAccountAlias> GetMailAliases(this ListSendAsResponse response, List<MailAccountAlias> currentAliases, MailAccount account)
{
if (response == null || response.SendAs == null) return currentAccount.Aliases;
if (response == null || response.SendAs == null) return currentAliases;
var remoteAliases = response.SendAs.Select(a => new MailAccountAlias()
{
AccountId = currentAccount.Id,
AccountId = account.Id,
AliasAddress = a.SendAsEmail,
IsPrimary = a.IsPrimary.GetValueOrDefault(),
ReplyToAddress = string.IsNullOrEmpty(a.ReplyToAddress) ? currentAccount.Address : a.ReplyToAddress,
ReplyToAddress = string.IsNullOrEmpty(a.ReplyToAddress) ? account.Address : a.ReplyToAddress,
IsVerified = string.IsNullOrEmpty(a.VerificationStatus) ? true : a.VerificationStatus == "accepted",
IsRootAlias = account.Address == a.SendAsEmail,
Id = Guid.NewGuid()
}).ToList();
return EntityExtensions.GetFinalAliasList(currentAccount.Aliases, remoteAliases);
return EntityExtensions.GetFinalAliasList(currentAliases, remoteAliases);
}
}
}

View File

@@ -40,6 +40,8 @@ namespace Wino.Core.Integration.Processors
/// <returns>All folders.</returns>
Task<List<MailItemFolder>> GetLocalFoldersAsync(Guid accountId);
Task<List<MailAccountAlias>> GetAccountAliasesAsync(Guid accountId);
Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(SynchronizationOptions options);
Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
@@ -179,6 +181,9 @@ namespace Wino.Core.Integration.Processors
=> AccountService.UpdateAccountAsync(account);
public Task UpdateAccountAliasesAsync(Guid accountId, List<MailAccountAlias> aliases)
=> AccountService.UpdateAccountAliases(accountId, aliases);
=> AccountService.UpdateAccountAliasesAsync(accountId, aliases);
public Task<List<MailAccountAlias>> GetAccountAliasesAsync(Guid accountId)
=> AccountService.GetAccountAliasesAsync(accountId);
}
}

View File

@@ -50,7 +50,7 @@ namespace Wino.Core.MenuItems
public string Base64ProfilePicture
{
get => Parameter.Name;
set => SetProperty(Parameter.ProfilePictureBase64, value, Parameter, (u, n) => u.ProfilePictureBase64 = n);
set => SetProperty(Parameter.Base64ProfilePictureData, value, Parameter, (u, n) => u.Base64ProfilePictureData = n);
}
public IEnumerable<MailAccount> HoldingAccounts => new List<MailAccount> { Parameter };
@@ -65,7 +65,7 @@ namespace Wino.Core.MenuItems
Parameter = account;
AccountName = account.Name;
AttentionReason = account.AttentionReason;
Base64ProfilePicture = account.ProfilePictureBase64;
Base64ProfilePicture = account.Base64ProfilePictureData;
if (SubMenuItems == null) return;

View File

@@ -9,6 +9,7 @@ using SqlKata;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Extensions;
using Wino.Messaging.Client.Accounts;
using Wino.Messaging.UI;
@@ -226,43 +227,37 @@ namespace Wino.Core.Services
if (account.MergedInboxId != null)
account.MergedInbox = await GetMergedInboxInformationAsync(account.MergedInboxId.Value);
// Load aliases
account.Aliases = await GetAccountAliases(account.Id, account.Address);
account.Preferences = await GetAccountPreferencesAsync(account.Id);
}
return accounts;
}
private async Task<List<MailAccountAlias>> GetAccountAliases(Guid accountId, string primaryAccountAddress)
public async Task CreateRootAliasAsync(Guid accountId, string address)
{
// By default all accounts must have at least 1 primary alias to create drafts for.
// If there's no alias, create one from the existing account address. Migration doesn't exists to create one for older messages.
var aliases = await Connection
.Table<MailAccountAlias>()
.Where(a => a.AccountId == accountId)
.ToListAsync()
.ConfigureAwait(false);
if (!aliases.Any())
var rootAlias = new MailAccountAlias()
{
var primaryAccountAlias = new MailAccountAlias()
{
Id = Guid.NewGuid(),
AccountId = accountId,
IsPrimary = true,
AliasAddress = primaryAccountAddress,
ReplyToAddress = primaryAccountAddress,
IsVerified = true,
};
AccountId = accountId,
AliasAddress = address,
IsPrimary = true,
IsRootAlias = true,
IsVerified = true,
ReplyToAddress = address,
Id = Guid.NewGuid()
};
await Connection.InsertAsync(primaryAccountAlias).ConfigureAwait(false);
aliases.Add(primaryAccountAlias);
}
await Connection.InsertAsync(rootAlias).ConfigureAwait(false);
return aliases;
Log.Information("Created root alias for the account {AccountId}", accountId);
}
public async Task<List<MailAccountAlias>> GetAccountAliasesAsync(Guid accountId)
{
var query = new Query(nameof(MailAccountAlias))
.Where(nameof(MailAccountAlias.AccountId), accountId)
.OrderByDesc(nameof(MailAccountAlias.IsRootAlias));
return await Connection.QueryAsync<MailAccountAlias>(query.GetRawQuery()).ConfigureAwait(false);
}
private Task<MergedInbox> GetMergedInboxInformationAsync(Guid mergedInboxId)
@@ -277,6 +272,7 @@ namespace Wino.Core.Services
await Connection.Table<TokenInformation>().Where(a => a.AccountId == account.Id).DeleteAsync();
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);
// Account belongs to a merged inbox.
// In case of there'll be a single account in the merged inbox, remove the merged inbox as well.
@@ -327,6 +323,19 @@ namespace Wino.Core.Services
ReportUIChange(new AccountRemovedMessage(account));
}
public async Task UpdateProfileInformationAsync(Guid accountId, ProfileInformation profileInformation)
{
var account = await GetAccountAsync(accountId).ConfigureAwait(false);
if (account != null)
{
account.SenderName = profileInformation.SenderName;
account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
await UpdateAccountAsync(account).ConfigureAwait(false);
}
}
public async Task<MailAccount> GetAccountAsync(Guid accountId)
{
var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);
@@ -359,7 +368,7 @@ namespace Wino.Core.Services
ReportUIChange(new AccountUpdatedMessage(account));
}
public async Task UpdateAccountAliases(Guid accountId, List<MailAccountAlias> aliases)
public async Task UpdateAccountAliasesAsync(Guid accountId, List<MailAccountAlias> aliases)
{
// Delete existing ones.
await Connection.Table<MailAccountAlias>().DeleteAsync(a => a.AccountId == accountId).ConfigureAwait(false);
@@ -371,6 +380,17 @@ namespace Wino.Core.Services
}
}
public async Task DeleteAccountAliasAsync(Guid aliasId)
{
// Create query to delete alias.
var query = new Query("MailAccountAlias")
.Where("Id", aliasId)
.AsDelete();
await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
}
public async Task CreateAccountAsync(MailAccount account, TokenInformation tokenInformation, CustomServerInformation customServerInformation)
{
Guard.IsNotNull(account);
@@ -424,7 +444,7 @@ namespace Wino.Core.Services
// Outlook token cache is managed by MSAL.
// Don't save it to database.
if (tokenInformation != null && account.ProviderType != MailProviderType.Outlook)
if (tokenInformation != null && (account.ProviderType != MailProviderType.Outlook || account.ProviderType == MailProviderType.Office365))
await Connection.InsertAsync(tokenInformation);
}

View File

@@ -13,6 +13,7 @@ using Wino.Core.Domain;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Integration;
@@ -71,10 +72,16 @@ namespace Wino.Core.Synchronizers
public abstract Task ExecuteNativeRequestsAsync(IEnumerable<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
/// <summary>
/// Refreshed remote mail account profile if possible.
/// Aliases, profile pictures, mailbox settings will be handled in this step.
/// Refreshes remote mail account profile if possible.
/// Profile picture, sender name and mailbox settings (todo) will be handled in this step.
/// </summary>
protected virtual Task SynchronizeProfileInformationAsync() => Task.CompletedTask;
public virtual Task<ProfileInformation> SynchronizeProfileInformationAsync() => default;
/// <summary>
/// Refreshes the aliases of the account.
/// Only available for Gmail right now.
/// </summary>
protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask;
/// <summary>
/// Returns the base64 encoded profile picture of the account from the given URL.
@@ -100,6 +107,33 @@ namespace Wino.Core.Synchronizers
/// <returns>Synchronization result that contains summary of the sync.</returns>
protected abstract Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Safely updates account's profile information.
/// Database changes are reflected after this call.
/// Null returns mean that the operation failed.
/// </summary>
private async Task<ProfileInformation> SynchronizeProfileInformationInternalAsync()
{
try
{
var profileInformation = await SynchronizeProfileInformationAsync();
if (profileInformation != null)
{
Account.SenderName = profileInformation.SenderName;
Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
}
return profileInformation;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to update profile information for account '{Name}'", Account.Name);
}
return null;
}
/// <summary>
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
/// </summary>
@@ -139,12 +173,16 @@ namespace Wino.Core.Synchronizers
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
if (options.Type == SynchronizationType.Full)
if (options.Type == SynchronizationType.UpdateProfile)
{
// Refresh profile information and mailbox settings on full synchronization.
// Refresh profile information on full synchronization.
// Exceptions here is not critical. Therefore, they are ignored.
await SynchronizeProfileInformationAsync();
var newprofileInformation = await SynchronizeProfileInformationInternalAsync();
if (newprofileInformation == null) return SynchronizationResult.Failed;
return SynchronizationResult.Completed(null, newprofileInformation);
}
// Let servers to finish their job. Sometimes the servers doesn't respond immediately.

View File

@@ -19,6 +19,7 @@ using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Synchronization;
@@ -68,67 +69,45 @@ namespace Wino.Core.Synchronizers
public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) => _googleHttpClient;
protected override async Task SynchronizeProfileInformationAsync()
public override async Task<ProfileInformation> SynchronizeProfileInformationAsync()
{
// Gmail profile info synchronizes Sender Name, Alias and Profile Picture.
var profileRequest = _peopleService.People.Get("people/me");
profileRequest.PersonFields = "names,photos";
try
string senderName = string.Empty, base64ProfilePicture = string.Empty;
var userProfile = await profileRequest.ExecuteAsync();
senderName = userProfile.Names?.FirstOrDefault()?.DisplayName ?? Account.SenderName;
var profilePicture = userProfile.Photos?.FirstOrDefault()?.Url ?? string.Empty;
if (!string.IsNullOrEmpty(profilePicture))
{
var profileRequest = _peopleService.People.Get("people/me");
profileRequest.PersonFields = "names,photos";
string senderName = Account.SenderName, base64ProfilePicture = Account.ProfilePictureBase64;
var userProfile = await profileRequest.ExecuteAsync();
senderName = userProfile.Names?.FirstOrDefault()?.DisplayName ?? Account.SenderName;
var profilePicture = userProfile.Photos?.FirstOrDefault()?.Url ?? string.Empty;
if (!string.IsNullOrEmpty(profilePicture))
{
base64ProfilePicture = await GetProfilePictureBase64EncodedAsync(profilePicture).ConfigureAwait(false);
}
bool shouldUpdateAccountProfile = (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName)
|| (!string.IsNullOrEmpty(profilePicture) && Account.ProfilePictureBase64 != base64ProfilePicture);
if (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName)
{
Account.SenderName = senderName;
}
if (!string.IsNullOrEmpty(profilePicture) && Account.ProfilePictureBase64 != base64ProfilePicture)
{
Account.ProfilePictureBase64 = base64ProfilePicture;
}
// Sync aliases
var sendAsListRequest = _gmailService.Users.Settings.SendAs.List("me");
var sendAsListResponse = await sendAsListRequest.ExecuteAsync();
var updatedAliases = sendAsListResponse.GetMailAliases(Account);
bool shouldUpdateAliases =
Account.Aliases.Any(a => updatedAliases.Any(b => a.Id == b.Id) == false) ||
updatedAliases.Any(a => Account.Aliases.Any(b => a.Id == b.Id) == false);
if (shouldUpdateAliases)
{
Account.Aliases = updatedAliases;
await _gmailChangeProcessor.UpdateAccountAliasesAsync(Account.Id, updatedAliases);
}
if (shouldUpdateAccountProfile)
{
await _gmailChangeProcessor.UpdateAccountAsync(Account).ConfigureAwait(false);
}
base64ProfilePicture = await GetProfilePictureBase64EncodedAsync(profilePicture).ConfigureAwait(false);
}
catch (Exception ex)
return new ProfileInformation(senderName, base64ProfilePicture);
}
protected override async Task SynchronizeAliasesAsync()
{
// Sync aliases
var sendAsListRequest = _gmailService.Users.Settings.SendAs.List("me");
var sendAsListResponse = await sendAsListRequest.ExecuteAsync();
var localAliases = await _gmailChangeProcessor.GetAccountAliasesAsync(Account.Id).ConfigureAwait(false);
var updatedAliases = sendAsListResponse.GetMailAliases(localAliases, Account);
bool shouldUpdateAliases =
localAliases.Any(a => updatedAliases.Any(b => a.Id == b.Id) == false) ||
updatedAliases.Any(a => localAliases.Any(b => a.Id == b.Id) == false);
if (shouldUpdateAliases)
{
Logger.Error(ex, "Error while synchronizing profile information for {Name}", Account.Name);
await _gmailChangeProcessor.UpdateAccountAliasesAsync(Account.Id, updatedAliases);
}
}

View File

@@ -23,6 +23,7 @@ using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Synchronization;
@@ -479,70 +480,28 @@ namespace Wino.Core.Synchronizers
/// <returns>Base64 encoded profile picture.</returns>
private async Task<string> GetUserProfilePictureAsync()
{
try
{
var photoStream = await _graphClient.Me.Photos["48x48"].Content.GetAsync();
var photoStream = await _graphClient.Me.Photos["48x48"].Content.GetAsync();
using var memoryStream = new MemoryStream();
await photoStream.CopyToAsync(memoryStream);
var byteArray = memoryStream.ToArray();
using var memoryStream = new MemoryStream();
await photoStream.CopyToAsync(memoryStream);
var byteArray = memoryStream.ToArray();
return Convert.ToBase64String(byteArray);
}
catch (Exception ex)
{
Log.Error(ex, "Error occurred while getting user profile picture.");
return string.Empty;
}
return Convert.ToBase64String(byteArray);
}
private async Task<string> GetSenderNameAsync()
{
try
{
var userInfo = await _graphClient.Users["me"].GetAsync();
var userInfo = await _graphClient.Users["me"].GetAsync();
return userInfo.DisplayName;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to get sender name.");
return string.Empty;
}
return userInfo.DisplayName;
}
protected override async Task SynchronizeProfileInformationAsync()
public override async Task<ProfileInformation> SynchronizeProfileInformationAsync()
{
try
{
// Outlook profile info synchronizes Sender Name and Profile Picture.
string senderName = Account.SenderName, base64ProfilePicture = Account.ProfilePictureBase64;
var profilePictureData = await GetUserProfilePictureAsync().ConfigureAwait(false);
var senderName = await GetSenderNameAsync().ConfigureAwait(false);
var profilePictureData = await GetUserProfilePictureAsync().ConfigureAwait(false);
senderName = await GetSenderNameAsync().ConfigureAwait(false);
bool shouldUpdateAccountProfile = (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName)
|| (!string.IsNullOrEmpty(profilePictureData) && Account.ProfilePictureBase64 != base64ProfilePicture);
if (!string.IsNullOrEmpty(profilePictureData) && Account.ProfilePictureBase64 != profilePictureData)
{
Account.ProfilePictureBase64 = profilePictureData;
}
if (!string.IsNullOrEmpty(senderName) && Account.SenderName != senderName)
{
Account.SenderName = senderName;
}
if (shouldUpdateAccountProfile)
{
await _outlookChangeProcessor.UpdateAccountAsync(Account).ConfigureAwait(false);
}
}
catch (Exception ex)
{
Log.Error(ex, "Failed to synchronize profile information for {Name}", Account.Name);
}
return new ProfileInformation(senderName, profilePictureData);
}
#region Mail Integration