diff --git a/Wino.Core.Domain/Entities/MailAccount.cs b/Wino.Core.Domain/Entities/MailAccount.cs
index ecd8fb64..60568d32 100644
--- a/Wino.Core.Domain/Entities/MailAccount.cs
+++ b/Wino.Core.Domain/Entities/MailAccount.cs
@@ -45,6 +45,11 @@ namespace Wino.Core.Domain.Entities
///
public string AccountColorHex { get; set; }
+ ///
+ /// Base64 encoded profile picture of the account.
+ ///
+ public string ProfilePictureBase64 { get; set; }
+
///
/// Gets or sets the listing order of the account in the accounts list.
///
diff --git a/Wino.Core.Domain/Models/Authorization/GoogleAuthorizationRequest.cs b/Wino.Core.Domain/Models/Authorization/GoogleAuthorizationRequest.cs
index 455fff47..bef11d59 100644
--- a/Wino.Core.Domain/Models/Authorization/GoogleAuthorizationRequest.cs
+++ b/Wino.Core.Domain/Models/Authorization/GoogleAuthorizationRequest.cs
@@ -33,7 +33,7 @@ namespace Wino.Core.Domain.Models.Authorization
ClientId = clientId;
// Creates the OAuth 2.0 authorization request.
- return string.Format("{0}?response_type=code&scope=https://mail.google.com/ https://www.googleapis.com/auth/gmail.labels&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}",
+ return string.Format("{0}?response_type=code&scope=https://mail.google.com/ https://www.googleapis.com/auth/gmail.labels https://www.googleapis.com/auth/userinfo.profile&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}",
authorizationEndpoint,
Uri.EscapeDataString(RedirectUri),
ClientId,
diff --git a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
index 14ebc1de..4028dc9c 100644
--- a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
+++ b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
@@ -19,6 +19,7 @@ namespace Wino.Core.Integration.Processors
///
public interface IDefaultChangeProcessor
{
+ Task UpdateAccountAsync(MailAccount account);
Task UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier);
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
@@ -172,5 +173,8 @@ namespace Wino.Core.Integration.Processors
public Task UpdateFolderLastSyncDateAsync(Guid folderId)
=> FolderService.UpdateFolderLastSyncDateAsync(folderId);
+
+ public Task UpdateAccountAsync(MailAccount account)
+ => AccountService.UpdateAccountAsync(account);
}
}
diff --git a/Wino.Core/MenuItems/AccountMenuItem.cs b/Wino.Core/MenuItems/AccountMenuItem.cs
index 045038f4..5cba23d3 100644
--- a/Wino.Core/MenuItems/AccountMenuItem.cs
+++ b/Wino.Core/MenuItems/AccountMenuItem.cs
@@ -47,6 +47,12 @@ namespace Wino.Core.MenuItems
set => SetProperty(Parameter.Name, value, Parameter, (u, n) => u.Name = n);
}
+ public string Base64ProfilePicture
+ {
+ get => Parameter.Name;
+ set => SetProperty(Parameter.ProfilePictureBase64, value, Parameter, (u, n) => u.ProfilePictureBase64 = n);
+ }
+
public IEnumerable HoldingAccounts => new List { Parameter };
public AccountMenuItem(MailAccount account, IMenuItem parent = null) : base(account, account.Id, parent)
@@ -59,6 +65,7 @@ namespace Wino.Core.MenuItems
Parameter = account;
AccountName = account.Name;
AttentionReason = account.AttentionReason;
+ Base64ProfilePicture = account.ProfilePictureBase64;
if (SubMenuItems == null) return;
diff --git a/Wino.Core/Synchronizers/BaseSynchronizer.cs b/Wino.Core/Synchronizers/BaseSynchronizer.cs
index acd7f213..055a4bb4 100644
--- a/Wino.Core/Synchronizers/BaseSynchronizer.cs
+++ b/Wino.Core/Synchronizers/BaseSynchronizer.cs
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
@@ -69,8 +70,42 @@ namespace Wino.Core.Synchronizers
/// Cancellation token
public abstract Task ExecuteNativeRequestsAsync(IEnumerable> batchedRequests, CancellationToken cancellationToken = default);
- public abstract Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
+ ///
+ /// Refreshed remote mail account profile if possible.
+ /// Aliases, profile pictures, mailbox settings will be handled in this step.
+ ///
+ protected virtual Task SynchronizeProfileInformationAsync() => Task.CompletedTask;
+ ///
+ /// Returns the base64 encoded profile picture of the account from the given URL.
+ ///
+ /// URL to retrieve picture from.
+ /// base64 encoded profile picture
+ protected async Task GetProfilePictureBase64EncodedAsync(string url)
+ {
+ using var client = new HttpClient();
+
+ var response = await client.GetAsync(url).ConfigureAwait(false);
+ var byteContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
+
+ return Convert.ToBase64String(byteContent);
+ }
+
+ ///
+ /// Internally synchronizes the account with the given options.
+ /// Not exposed and overriden for each synchronizer.
+ ///
+ /// Synchronization options.
+ /// Cancellation token.
+ /// Synchronization result that contains summary of the sync.
+ protected abstract Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
+
+ ///
+ /// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
+ ///
+ /// Synchronization options.
+ /// Cancellation token.
+ /// Synchronization result that contains summary of the sync.
public async Task SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
try
@@ -104,6 +139,14 @@ namespace Wino.Core.Synchronizers
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
+ if (options.Type == SynchronizationType.Full)
+ {
+ // Refresh profile information and mailbox settings on full synchronization.
+ // Exceptions here is not critical. Therefore, they are ignored.
+
+ await SynchronizeProfileInformationAsync();
+ }
+
// Let servers to finish their job. Sometimes the servers doesn't respond immediately.
bool shouldDelayExecution = batches.Any(a => a.DelayExecution);
@@ -150,6 +193,10 @@ namespace Wino.Core.Synchronizers
private void PublishUnreadItemChanges()
=> WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(Account.Id));
+ ///
+ /// Sends a message to the shell to update the synchronization progress.
+ ///
+ /// Percentage of the progress.
public void PublishSynchronizationProgress(double progress)
=> WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress));
diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs
index 3ee7e044..106a8413 100644
--- a/Wino.Core/Synchronizers/GmailSynchronizer.cs
+++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Google.Apis.Gmail.v1;
using Google.Apis.Gmail.v1.Data;
using Google.Apis.Http;
+using Google.Apis.PeopleService.v1;
using Google.Apis.Requests;
using Google.Apis.Services;
using MailKit;
@@ -37,8 +38,10 @@ namespace Wino.Core.Synchronizers
// https://github.com/googleapis/google-api-dotnet-client/issues/2603
private const uint MaximumAllowedBatchRequestSize = 10;
- private readonly ConfigurableHttpClient _gmailHttpClient;
+ private readonly ConfigurableHttpClient _googleHttpClient;
private readonly GmailService _gmailService;
+ private readonly PeopleServiceService _peopleService;
+
private readonly IAuthenticator _authenticator;
private readonly IGmailChangeProcessor _gmailChangeProcessor;
private readonly ILogger _logger = Log.ForContext();
@@ -54,15 +57,64 @@ namespace Wino.Core.Synchronizers
HttpClientFactory = this
};
- _gmailHttpClient = new ConfigurableHttpClient(messageHandler);
+ _googleHttpClient = new ConfigurableHttpClient(messageHandler);
+
_gmailService = new GmailService(initializer);
+ _peopleService = new PeopleServiceService(initializer);
+
_authenticator = authenticator;
_gmailChangeProcessor = gmailChangeProcessor;
}
- public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) => _gmailHttpClient;
+ public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) => _googleHttpClient;
- public override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
+ protected override async Task SynchronizeProfileInformationAsync()
+ {
+ // Gmail profile info synchronizes Alias and Profile Picture.
+
+ try
+ {
+ 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;
+ }
+
+ if (shouldUpdateAccountProfile)
+ {
+ await _gmailChangeProcessor.UpdateAccountAsync(Account).ConfigureAwait(false);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, "Error while synchronizing profile information for {Name}", Account.Name);
+ }
+ }
+
+ protected override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
_logger.Information("Internal synchronization started for {Name}", Account.Name);
diff --git a/Wino.Core/Synchronizers/ImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSynchronizer.cs
index 0b8cd2f6..e0ef6810 100644
--- a/Wino.Core/Synchronizers/ImapSynchronizer.cs
+++ b/Wino.Core/Synchronizers/ImapSynchronizer.cs
@@ -405,7 +405,7 @@ namespace Wino.Core.Synchronizers
];
}
- public override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
+ protected override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
var downloadedMessageIds = new List();
diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
index 554e9dcf..c9872d3f 100644
--- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs
+++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
@@ -128,7 +128,7 @@ namespace Wino.Core.Synchronizers
#endregion
- public override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
+ protected override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
var downloadedMessageIds = new List();
diff --git a/Wino.Core/Wino.Core.csproj b/Wino.Core/Wino.Core.csproj
index be8ca55c..476216e3 100644
--- a/Wino.Core/Wino.Core.csproj
+++ b/Wino.Core/Wino.Core.csproj
@@ -17,6 +17,7 @@
+