Managing account aliases and profile synchronization for outlook and gmail.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user