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

@@ -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