2024-04-18 01:44:37 +02:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Net.Http ;
using System.Threading ;
using System.Threading.Tasks ;
2024-08-24 17:22:47 +02:00
using CommunityToolkit.Mvvm.Messaging ;
2024-11-30 12:47:24 +01:00
using Google.Apis.Calendar.v3.Data ;
2024-04-18 01:44:37 +02:00
using Google.Apis.Gmail.v1 ;
using Google.Apis.Gmail.v1.Data ;
using Google.Apis.Http ;
2024-08-15 23:57:45 +02:00
using Google.Apis.PeopleService.v1 ;
2024-04-18 01:44:37 +02:00
using Google.Apis.Requests ;
using Google.Apis.Services ;
using MailKit ;
using Microsoft.IdentityModel.Tokens ;
using MimeKit ;
using MoreLinq ;
using Serilog ;
2024-12-27 00:18:46 +01:00
using Wino.Core.Domain.Entities.Calendar ;
2024-11-10 23:28:25 +01:00
using Wino.Core.Domain.Entities.Mail ;
using Wino.Core.Domain.Entities.Shared ;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Enums ;
using Wino.Core.Domain.Exceptions ;
using Wino.Core.Domain.Interfaces ;
2024-08-17 03:43:37 +02:00
using Wino.Core.Domain.Models.Accounts ;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Models.MailItem ;
using Wino.Core.Domain.Models.Synchronization ;
using Wino.Core.Extensions ;
using Wino.Core.Http ;
using Wino.Core.Integration.Processors ;
2024-11-10 23:28:25 +01:00
using Wino.Core.Requests.Bundles ;
2024-11-26 20:03:10 +01:00
using Wino.Core.Requests.Folder ;
using Wino.Core.Requests.Mail ;
2024-08-24 17:22:47 +02:00
using Wino.Messaging.UI ;
2024-11-30 23:05:07 +01:00
using Wino.Services ;
2024-12-27 00:18:46 +01:00
using CalendarService = Google . Apis . Calendar . v3 . CalendarService ;
2024-04-18 01:44:37 +02:00
2024-11-10 23:28:25 +01:00
namespace Wino.Core.Synchronizers.Mail
2024-04-18 01:44:37 +02:00
{
2024-11-30 12:47:24 +01:00
public class GmailSynchronizer : WinoSynchronizer < IClientServiceRequest , Message , Event > , IHttpClientFactory
2024-04-18 01:44:37 +02:00
{
public override uint BatchModificationSize = > 1000 ;
public override uint InitialMessageDownloadCountPerFolder = > 1200 ;
// It's actually 100. But Gmail SDK has internal bug for Out of Memory exception.
// https://github.com/googleapis/google-api-dotnet-client/issues/2603
private const uint MaximumAllowedBatchRequestSize = 10 ;
2024-08-15 23:57:45 +02:00
private readonly ConfigurableHttpClient _googleHttpClient ;
2024-04-18 01:44:37 +02:00
private readonly GmailService _gmailService ;
2024-12-27 00:18:46 +01:00
private readonly CalendarService _calendarService ;
2024-08-15 23:57:45 +02:00
private readonly PeopleServiceService _peopleService ;
2024-06-02 21:35:03 +02:00
private readonly IGmailChangeProcessor _gmailChangeProcessor ;
2024-04-18 01:44:37 +02:00
private readonly ILogger _logger = Log . ForContext < GmailSynchronizer > ( ) ;
public GmailSynchronizer ( MailAccount account ,
2024-11-20 01:45:48 +01:00
IGmailAuthenticator authenticator ,
2024-06-02 21:35:03 +02:00
IGmailChangeProcessor gmailChangeProcessor ) : base ( account )
2024-04-18 01:44:37 +02:00
{
2024-11-20 01:45:48 +01:00
var messageHandler = new GmailClientMessageHandler ( authenticator , account ) ;
2024-04-18 01:44:37 +02:00
var initializer = new BaseClientService . Initializer ( )
{
HttpClientFactory = this
} ;
2024-08-15 23:57:45 +02:00
_googleHttpClient = new ConfigurableHttpClient ( messageHandler ) ;
2024-04-18 01:44:37 +02:00
_gmailService = new GmailService ( initializer ) ;
2024-08-15 23:57:45 +02:00
_peopleService = new PeopleServiceService ( initializer ) ;
2024-12-27 00:18:46 +01:00
_calendarService = new CalendarService ( initializer ) ;
2024-04-18 01:44:37 +02:00
_gmailChangeProcessor = gmailChangeProcessor ;
}
2024-08-15 23:57:45 +02:00
public ConfigurableHttpClient CreateHttpClient ( CreateHttpClientArgs args ) = > _googleHttpClient ;
2024-08-17 19:54:52 +02:00
public override async Task < ProfileInformation > GetProfileInformationAsync ( )
2024-08-15 23:57:45 +02:00
{
2024-08-17 03:43:37 +02:00
var profileRequest = _peopleService . People . Get ( "people/me" ) ;
2024-12-01 03:05:15 +01:00
profileRequest . PersonFields = "names,photos,emailAddresses" ;
2024-08-15 23:57:45 +02:00
2024-11-20 01:45:48 +01:00
string senderName = string . Empty , base64ProfilePicture = string . Empty , address = string . Empty ;
2024-08-15 23:57:45 +02:00
2024-08-17 03:43:37 +02:00
var userProfile = await profileRequest . ExecuteAsync ( ) ;
2024-08-15 23:57:45 +02:00
2024-08-17 03:43:37 +02:00
senderName = userProfile . Names ? . FirstOrDefault ( ) ? . DisplayName ? ? Account . SenderName ;
2024-08-15 23:57:45 +02:00
2024-08-17 03:43:37 +02:00
var profilePicture = userProfile . Photos ? . FirstOrDefault ( ) ? . Url ? ? string . Empty ;
2024-08-15 23:57:45 +02:00
2024-08-17 03:43:37 +02:00
if ( ! string . IsNullOrEmpty ( profilePicture ) )
{
base64ProfilePicture = await GetProfilePictureBase64EncodedAsync ( profilePicture ) . ConfigureAwait ( false ) ;
}
2024-08-15 23:57:45 +02:00
2024-12-21 19:31:24 +01:00
address = userProfile . EmailAddresses . FirstOrDefault ( a = > a . Metadata . Primary = = true ) . Value ;
2024-12-01 03:05:15 +01:00
2024-11-20 01:45:48 +01:00
return new ProfileInformation ( senderName , base64ProfilePicture , address ) ;
2024-08-17 03:43:37 +02:00
}
2024-08-16 01:29:31 +02:00
2024-08-17 03:43:37 +02:00
protected override async Task SynchronizeAliasesAsync ( )
{
var sendAsListRequest = _gmailService . Users . Settings . SendAs . List ( "me" ) ;
var sendAsListResponse = await sendAsListRequest . ExecuteAsync ( ) ;
2024-08-17 19:54:52 +02:00
var remoteAliases = sendAsListResponse . GetRemoteAliases ( ) ;
2024-08-16 01:29:31 +02:00
2024-08-17 19:54:52 +02:00
await _gmailChangeProcessor . UpdateRemoteAliasInformationAsync ( Account , remoteAliases ) . ConfigureAwait ( false ) ;
2024-08-15 23:57:45 +02:00
}
2024-04-18 01:44:37 +02:00
2024-12-24 18:30:25 +01:00
protected override async Task < MailSynchronizationResult > SynchronizeMailsInternalAsync ( MailSynchronizationOptions options , CancellationToken cancellationToken = default )
2024-04-18 01:44:37 +02:00
{
2024-12-24 18:30:25 +01:00
_logger . Information ( "Internal mail synchronization started for {Name}" , Account . Name ) ;
2024-04-18 01:44:37 +02:00
// Gmail must always synchronize folders before because it doesn't have a per-folder sync.
bool shouldSynchronizeFolders = true ;
if ( shouldSynchronizeFolders )
{
_logger . Information ( "Synchronizing folders for {Name}" , Account . Name ) ;
await SynchronizeFoldersAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
_logger . Information ( "Synchronizing folders for {Name} is completed" , Account . Name ) ;
}
// There is no specific folder synchronization in Gmail.
// Therefore we need to stop the synchronization at this point
// if type is only folder metadata sync.
2024-12-24 18:30:25 +01:00
if ( options . Type = = MailSynchronizationType . FoldersOnly ) return MailSynchronizationResult . Empty ;
2024-04-18 01:44:37 +02:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
bool isInitialSync = string . IsNullOrEmpty ( Account . SynchronizationDeltaIdentifier ) ;
_logger . Debug ( "Is initial synchronization: {IsInitialSync}" , isInitialSync ) ;
var missingMessageIds = new List < string > ( ) ;
var deltaChanges = new List < ListHistoryResponse > ( ) ; // For tracking delta changes.
var listChanges = new List < ListMessagesResponse > ( ) ; // For tracking initial sync changes.
/ * Processing flow order is important to preserve the validity of history .
* 1 - Process added mails . Because we need to create the mail first before assigning it to labels .
* 2 - Process label assignments .
* 3 - Process removed mails .
* This affects reporting progres if done individually for each history change .
* Therefore we need to process all changes in one go after the fetch .
* /
if ( isInitialSync )
{
// Initial synchronization.
// Google sends message id and thread id in this query.
// We'll collect them and send a Batch request to get details of the messages.
var messageRequest = _gmailService . Users . Messages . List ( "me" ) ;
// Gmail doesn't do per-folder sync. So our per-folder count is the same as total message count.
messageRequest . MaxResults = InitialMessageDownloadCountPerFolder ;
messageRequest . IncludeSpamTrash = true ;
ListMessagesResponse result = null ;
string nextPageToken = string . Empty ;
while ( true )
{
if ( ! string . IsNullOrEmpty ( nextPageToken ) )
{
messageRequest . PageToken = nextPageToken ;
}
result = await messageRequest . ExecuteAsync ( cancellationToken ) ;
nextPageToken = result . NextPageToken ;
listChanges . Add ( result ) ;
// Nothing to fetch anymore. Break the loop.
if ( nextPageToken = = null )
break ;
}
}
else
{
var startHistoryId = ulong . Parse ( Account . SynchronizationDeltaIdentifier ) ;
var nextPageToken = ulong . Parse ( Account . SynchronizationDeltaIdentifier ) . ToString ( ) ;
var historyRequest = _gmailService . Users . History . List ( "me" ) ;
historyRequest . StartHistoryId = startHistoryId ;
while ( ! string . IsNullOrEmpty ( nextPageToken ) )
{
// If this is the first delta check, start from the last history id.
// Otherwise start from the next page token. We set them both to the same value for start.
// For each different page we set the page token to the next page token.
bool isFirstDeltaCheck = nextPageToken = = startHistoryId . ToString ( ) ;
if ( ! isFirstDeltaCheck )
historyRequest . PageToken = nextPageToken ;
var historyResponse = await historyRequest . ExecuteAsync ( cancellationToken ) ;
nextPageToken = historyResponse . NextPageToken ;
if ( historyResponse . History = = null )
continue ;
deltaChanges . Add ( historyResponse ) ;
}
}
// Add initial message ids from initial sync.
missingMessageIds . AddRange ( listChanges . Where ( a = > a . Messages ! = null ) . SelectMany ( a = > a . Messages ) . Select ( a = > a . Id ) ) ;
// Add missing message ids from delta changes.
foreach ( var historyResponse in deltaChanges )
{
var addedMessageIds = historyResponse . History
. Where ( a = > a . MessagesAdded ! = null )
. SelectMany ( a = > a . MessagesAdded )
. Where ( a = > a . Message ! = null )
. Select ( a = > a . Message . Id ) ;
missingMessageIds . AddRange ( addedMessageIds ) ;
}
// Consolidate added/deleted elements.
// For example: History change might report downloading a mail first, then deleting it in another history change.
// In that case, downloading mail will return entity not found error.
// Plus, it's a redundant download the mail.
// Purge missing message ids from potentially deleted mails to prevent this.
var messageDeletedHistoryChanges = deltaChanges
. Where ( a = > a . History ! = null )
. SelectMany ( a = > a . History )
. Where ( a = > a . MessagesDeleted ! = null )
. SelectMany ( a = > a . MessagesDeleted ) ;
var deletedMailIdsInHistory = messageDeletedHistoryChanges . Select ( a = > a . Message . Id ) ;
if ( deletedMailIdsInHistory . Any ( ) )
{
var mailIdsToConsolidate = missingMessageIds . Where ( a = > deletedMailIdsInHistory . Contains ( a ) ) . ToList ( ) ;
int consolidatedMessageCount = missingMessageIds . RemoveAll ( a = > deletedMailIdsInHistory . Contains ( a ) ) ;
if ( consolidatedMessageCount > 0 )
{
// TODO: Also delete the history changes that are related to these mails.
// This will prevent unwanted logs and additional queries to look for them in processing.
_logger . Information ( $"Purged {consolidatedMessageCount} missing mail downloads. ({string.Join(" , ", mailIdsToConsolidate)})" ) ;
}
}
// Start downloading missing messages.
2024-08-05 00:36:26 +02:00
await BatchDownloadMessagesAsync ( missingMessageIds , cancellationToken ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
// Map remote drafts to local drafts.
await MapDraftIdsAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
// Start processing delta changes.
foreach ( var historyResponse in deltaChanges )
{
await ProcessHistoryChangesAsync ( historyResponse ) . ConfigureAwait ( false ) ;
}
// Take the max history id from delta changes and update the account sync modifier.
var maxHistoryId = deltaChanges . Max ( a = > a . HistoryId ) ;
if ( maxHistoryId ! = null )
{
// TODO: This is not good. Centralize the identifier fetch and prevent direct access here.
Account . SynchronizationDeltaIdentifier = await _gmailChangeProcessor . UpdateAccountDeltaSynchronizationIdentifierAsync ( Account . Id , maxHistoryId . ToString ( ) ) . ConfigureAwait ( false ) ;
_logger . Debug ( "Final sync identifier {SynchronizationDeltaIdentifier}" , Account . SynchronizationDeltaIdentifier ) ;
}
// Get all unred new downloaded items and return in the result.
// This is primarily used in notifications.
var unreadNewItems = await _gmailChangeProcessor . GetDownloadedUnreadMailsAsync ( Account . Id , missingMessageIds ) . ConfigureAwait ( false ) ;
2024-12-24 18:30:25 +01:00
return MailSynchronizationResult . Completed ( unreadNewItems ) ;
}
2024-12-27 00:18:46 +01:00
protected override async Task < CalendarSynchronizationResult > SynchronizeCalendarEventsInternalAsync ( CalendarSynchronizationOptions options , CancellationToken cancellationToken = default )
2024-12-24 18:30:25 +01:00
{
_logger . Information ( "Internal calendar synchronization started for {Name}" , Account . Name ) ;
2024-12-27 00:18:46 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2024-04-18 01:44:37 +02:00
2024-12-27 00:18:46 +01:00
await SynchronizeCalendarsAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
bool isInitialSync = string . IsNullOrEmpty ( Account . SynchronizationDeltaIdentifier ) ;
_logger . Debug ( "Is initial synchronization: {IsInitialSync}" , isInitialSync ) ;
var localCalendars = await _gmailChangeProcessor . GetAccountCalendarsAsync ( Account . Id ) . ConfigureAwait ( false ) ;
// TODO: Better logging and exception handling.
foreach ( var calendar in localCalendars )
2024-04-18 01:44:37 +02:00
{
2024-12-27 00:18:46 +01:00
var request = _calendarService . Events . List ( calendar . RemoteCalendarId ) ;
2024-04-18 01:44:37 +02:00
2024-12-27 00:18:46 +01:00
request . SingleEvents = false ;
request . ShowDeleted = true ;
2024-04-18 01:44:37 +02:00
2024-12-27 00:18:46 +01:00
if ( ! string . IsNullOrEmpty ( calendar . SynchronizationDeltaToken ) )
{
// If a sync token is available, perform an incremental sync
request . SyncToken = calendar . SynchronizationDeltaToken ;
}
else
2024-07-09 01:05:16 +02:00
{
2024-12-27 00:18:46 +01:00
// If no sync token, perform an initial sync
// Fetch events from the past year
request . TimeMinDateTimeOffset = DateTimeOffset . UtcNow . AddYears ( - 1 ) ;
2024-07-09 01:05:16 +02:00
}
2024-04-18 01:44:37 +02:00
2024-12-27 00:18:46 +01:00
string nextPageToken ;
string syncToken ;
2024-04-18 01:44:37 +02:00
2024-12-27 00:18:46 +01:00
var allEvents = new List < Event > ( ) ;
2024-04-18 01:44:37 +02:00
2024-12-27 00:18:46 +01:00
do
2024-07-09 01:05:16 +02:00
{
2024-12-27 00:18:46 +01:00
// Execute the request
var events = await request . ExecuteAsync ( ) ;
2024-04-18 01:44:37 +02:00
2024-12-27 00:18:46 +01:00
// Process the fetched events
if ( events . Items ! = null )
2024-07-09 01:05:16 +02:00
{
2024-12-27 00:18:46 +01:00
allEvents . AddRange ( events . Items ) ;
2024-07-09 01:05:16 +02:00
}
2024-12-27 00:18:46 +01:00
// Get the next page token and sync token
nextPageToken = events . NextPageToken ;
syncToken = events . NextSyncToken ;
// Set the next page token for subsequent requests
request . PageToken = nextPageToken ;
} while ( ! string . IsNullOrEmpty ( nextPageToken ) ) ;
calendar . SynchronizationDeltaToken = syncToken ;
2025-01-04 11:39:32 +01:00
// allEvents contains new or updated events.
// Process them and create/update local calendar items.
2024-12-27 00:18:46 +01:00
foreach ( var @event in allEvents )
{
// TODO: Exception handling for event processing.
2025-01-04 11:39:32 +01:00
// TODO: Also update attendees and other properties.
await _gmailChangeProcessor . ManageCalendarEventAsync ( @event , calendar , Account ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
}
2025-01-04 11:39:32 +01:00
await _gmailChangeProcessor . UpdateAccountCalendarAsync ( calendar ) . ConfigureAwait ( false ) ;
2024-12-27 00:18:46 +01:00
}
return default ;
}
private async Task SynchronizeCalendarsAsync ( CancellationToken cancellationToken = default )
{
var calendarListRequest = _calendarService . CalendarList . List ( ) ;
var calendarListResponse = await calendarListRequest . ExecuteAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( calendarListResponse . Items = = null )
{
_logger . Warning ( "No calendars found for {Name}" , Account . Name ) ;
return ;
}
var localCalendars = await _gmailChangeProcessor . GetAccountCalendarsAsync ( Account . Id ) . ConfigureAwait ( false ) ;
List < AccountCalendar > insertedCalendars = new ( ) ;
List < AccountCalendar > updatedCalendars = new ( ) ;
List < AccountCalendar > deletedCalendars = new ( ) ;
2024-04-18 01:44:37 +02:00
2024-12-27 00:18:46 +01:00
// 1. Handle deleted calendars.
2024-04-18 01:44:37 +02:00
2024-12-27 00:18:46 +01:00
foreach ( var calendar in localCalendars )
{
var remoteCalendar = calendarListResponse . Items . FirstOrDefault ( a = > a . Id = = calendar . RemoteCalendarId ) ;
if ( remoteCalendar = = null )
2024-04-18 01:44:37 +02:00
{
2024-12-27 00:18:46 +01:00
// Local calendar doesn't exists remotely. Delete local copy.
await _gmailChangeProcessor . DeleteAccountCalendarAsync ( calendar ) . ConfigureAwait ( false ) ;
deletedCalendars . Add ( calendar ) ;
}
}
// Delete the deleted folders from local list.
deletedCalendars . ForEach ( a = > localCalendars . Remove ( a ) ) ;
2024-04-18 01:44:37 +02:00
2024-12-27 00:18:46 +01:00
// 2. Handle update/insert based on remote calendars.
foreach ( var calendar in calendarListResponse . Items )
{
var existingLocalCalendar = localCalendars . FirstOrDefault ( a = > a . RemoteCalendarId = = calendar . Id ) ;
if ( existingLocalCalendar = = null )
{
// Insert new calendar.
var localCalendar = calendar . AsCalendar ( Account . Id ) ;
insertedCalendars . Add ( localCalendar ) ;
}
else
{
// Update existing calendar. Right now we only update the name.
if ( ShouldUpdateCalendar ( calendar , existingLocalCalendar ) )
2024-04-18 01:44:37 +02:00
{
2024-12-27 00:18:46 +01:00
existingLocalCalendar . Name = calendar . Summary ;
2024-04-18 01:44:37 +02:00
2024-12-27 00:18:46 +01:00
updatedCalendars . Add ( existingLocalCalendar ) ;
2024-07-09 01:05:16 +02:00
}
else
{
2024-12-27 00:18:46 +01:00
// Remove it from the local folder list to skip additional calendar updates.
localCalendars . Remove ( existingLocalCalendar ) ;
2024-04-18 01:44:37 +02:00
}
}
2024-12-27 00:18:46 +01:00
}
2024-04-18 01:44:37 +02:00
2024-12-27 00:18:46 +01:00
// 3.Process changes in order-> Insert, Update. Deleted ones are already processed.
foreach ( var calendar in insertedCalendars )
{
await _gmailChangeProcessor . InsertAccountCalendarAsync ( calendar ) . ConfigureAwait ( false ) ;
}
2024-07-09 01:05:16 +02:00
2024-12-27 00:18:46 +01:00
foreach ( var calendar in updatedCalendars )
{
await _gmailChangeProcessor . UpdateAccountCalendarAsync ( calendar ) . ConfigureAwait ( false ) ;
}
if ( insertedCalendars . Any ( ) | | deletedCalendars . Any ( ) | | updatedCalendars . Any ( ) )
{
// TODO: Notify calendar updates.
// WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id));
}
}
private async Task SynchronizeFoldersAsync ( CancellationToken cancellationToken = default )
{
var localFolders = await _gmailChangeProcessor . GetLocalFoldersAsync ( Account . Id ) . ConfigureAwait ( false ) ;
var folderRequest = _gmailService . Users . Labels . List ( "me" ) ;
var labelsResponse = await folderRequest . ExecuteAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( labelsResponse . Labels = = null )
{
_logger . Warning ( "No folders found for {Name}" , Account . Name ) ;
return ;
}
List < MailItemFolder > insertedFolders = new ( ) ;
List < MailItemFolder > updatedFolders = new ( ) ;
List < MailItemFolder > deletedFolders = new ( ) ;
// 1. Handle deleted labels.
foreach ( var localFolder in localFolders )
{
// Category folder is virtual folder for Wino. Skip it.
if ( localFolder . SpecialFolderType = = SpecialFolderType . Category ) continue ;
var remoteFolder = labelsResponse . Labels . FirstOrDefault ( a = > a . Id = = localFolder . RemoteFolderId ) ;
if ( remoteFolder = = null )
2024-07-09 01:05:16 +02:00
{
2024-12-27 00:18:46 +01:00
// Local folder doesn't exists remotely. Delete local copy.
await _gmailChangeProcessor . DeleteFolderAsync ( Account . Id , localFolder . RemoteFolderId ) . ConfigureAwait ( false ) ;
deletedFolders . Add ( localFolder ) ;
2024-07-09 01:05:16 +02:00
}
2024-12-27 00:18:46 +01:00
}
// Delete the deleted folders from local list.
deletedFolders . ForEach ( a = > localFolders . Remove ( a ) ) ;
// 2. Handle update/insert based on remote folders.
foreach ( var remoteFolder in labelsResponse . Labels )
{
var existingLocalFolder = localFolders . FirstOrDefault ( a = > a . RemoteFolderId = = remoteFolder . Id ) ;
2024-07-09 01:05:16 +02:00
2024-12-27 00:18:46 +01:00
if ( existingLocalFolder = = null )
2024-07-09 01:05:16 +02:00
{
2024-12-27 00:18:46 +01:00
// Insert new folder.
var localFolder = remoteFolder . GetLocalFolder ( labelsResponse , Account . Id ) ;
2024-08-24 17:22:47 +02:00
2024-12-27 00:18:46 +01:00
insertedFolders . Add ( localFolder ) ;
}
else
2024-08-24 17:22:47 +02:00
{
2024-12-27 00:18:46 +01:00
// Update existing folder. Right now we only update the name.
// TODO: Moving folders around different parents. This is not supported right now.
// We will need more comphrensive folder update mechanism to support this.
if ( ShouldUpdateFolder ( remoteFolder , existingLocalFolder ) )
{
existingLocalFolder . FolderName = remoteFolder . Name ;
existingLocalFolder . TextColorHex = remoteFolder . Color ? . TextColor ;
existingLocalFolder . BackgroundColorHex = remoteFolder . Color ? . BackgroundColor ;
updatedFolders . Add ( existingLocalFolder ) ;
}
else
{
// Remove it from the local folder list to skip additional folder updates.
localFolders . Remove ( existingLocalFolder ) ;
}
2024-08-24 17:22:47 +02:00
}
2024-07-09 01:05:16 +02:00
}
2024-12-27 00:18:46 +01:00
// 3.Process changes in order-> Insert, Update. Deleted ones are already processed.
foreach ( var folder in insertedFolders )
2024-07-09 01:05:16 +02:00
{
2024-12-27 00:18:46 +01:00
await _gmailChangeProcessor . InsertFolderAsync ( folder ) . ConfigureAwait ( false ) ;
2024-07-09 01:05:16 +02:00
}
2024-12-27 00:18:46 +01:00
foreach ( var folder in updatedFolders )
{
await _gmailChangeProcessor . UpdateFolderAsync ( folder ) . ConfigureAwait ( false ) ;
}
if ( insertedFolders . Any ( ) | | deletedFolders . Any ( ) | | updatedFolders . Any ( ) )
{
WeakReferenceMessenger . Default . Send ( new AccountFolderConfigurationUpdated ( Account . Id ) ) ;
}
}
private bool ShouldUpdateCalendar ( CalendarListEntry calendarListEntry , AccountCalendar accountCalendar )
{
// TODO: Only calendar name is updated for now. We can add more checks here.
var remoteCalendarName = calendarListEntry . Summary ;
var localCalendarName = accountCalendar . Name ;
return ! localCalendarName . Equals ( remoteCalendarName , StringComparison . OrdinalIgnoreCase ) ;
2024-04-18 01:44:37 +02:00
}
2024-07-09 01:05:16 +02:00
private bool ShouldUpdateFolder ( Label remoteFolder , MailItemFolder existingLocalFolder )
2024-08-24 15:26:08 +02:00
{
2024-08-24 17:22:47 +02:00
var remoteFolderName = GoogleIntegratorExtensions . GetFolderName ( remoteFolder . Name ) ;
var localFolderName = GoogleIntegratorExtensions . GetFolderName ( existingLocalFolder . FolderName ) ;
bool isNameChanged = ! localFolderName . Equals ( remoteFolderName , StringComparison . OrdinalIgnoreCase ) ;
2024-08-24 15:26:08 +02:00
bool isColorChanged = existingLocalFolder . BackgroundColorHex ! = remoteFolder . Color ? . BackgroundColor | |
existingLocalFolder . TextColorHex ! = remoteFolder . Color ? . TextColor ;
return isNameChanged | | isColorChanged ;
}
2024-07-09 01:05:16 +02:00
2024-04-18 01:44:37 +02:00
/// <summary>
/// Returns a single get request to retrieve the raw message with the given id
/// </summary>
/// <param name="messageId">Message to download.</param>
/// <returns>Get request for raw mail.</returns>
private UsersResource . MessagesResource . GetRequest CreateSingleMessageGet ( string messageId )
{
var singleRequest = _gmailService . Users . Messages . Get ( "me" , messageId ) ;
singleRequest . Format = UsersResource . MessagesResource . GetRequest . FormatEnum . Raw ;
return singleRequest ;
}
/// <summary>
/// Downloads given message ids per batch and processes them.
/// </summary>
/// <param name="messageIds">Gmail message ids to download.</param>
/// <param name="cancellationToken">Cancellation token.</param>
2024-08-05 00:36:26 +02:00
private async Task BatchDownloadMessagesAsync ( IEnumerable < string > messageIds , CancellationToken cancellationToken = default )
2024-04-18 01:44:37 +02:00
{
var totalDownloadCount = messageIds . Count ( ) ;
if ( totalDownloadCount = = 0 ) return ;
var downloadedItemCount = 0 ;
_logger . Debug ( "Batch downloading {Count} messages for {Name}" , messageIds . Count ( ) , Account . Name ) ;
var allDownloadRequests = messageIds . Select ( CreateSingleMessageGet ) ;
// Respect the batch size limit for batch requests.
var batchedDownloadRequests = allDownloadRequests . Batch ( ( int ) MaximumAllowedBatchRequestSize ) ;
_logger . Debug ( "Total items to download: {TotalDownloadCount}. Created {Count} batch download requests for {Name}." , batchedDownloadRequests . Count ( ) , Account . Name , totalDownloadCount ) ;
// Gmail SDK's BatchRequest has Action delegate for callback, not Task.
// Therefore it's not possible to make sure that downloaded item is processed in the database before this
// async callback is finished. Therefore we need to wrap all local database processings into task list and wait all of them to finish
// Batch execution finishes after response parsing is done.
var batchProcessCallbacks = new List < Task > ( ) ;
foreach ( var batchBundle in batchedDownloadRequests )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
var batchRequest = new BatchRequest ( _gmailService ) ;
// Queue each request into this batch.
batchBundle . ForEach ( request = >
{
batchRequest . Queue < Message > ( request , ( content , error , index , message ) = >
{
var downloadingMessageId = messageIds . ElementAt ( index ) ;
batchProcessCallbacks . Add ( HandleSingleItemDownloadedCallbackAsync ( content , error , downloadingMessageId , cancellationToken ) ) ;
downloadedItemCount + + ;
var progressValue = downloadedItemCount * 100 / Math . Max ( 1 , totalDownloadCount ) ;
2024-08-05 00:36:26 +02:00
PublishSynchronizationProgress ( progressValue ) ;
2024-04-18 01:44:37 +02:00
} ) ;
} ) ;
_logger . Information ( "Executing batch download with {Count} items." , batchRequest . Count ) ;
await batchRequest . ExecuteAsync ( cancellationToken ) ;
// This is important due to bug in Gmail SDK.
// We force GC here to prevent Out of Memory exception.
// https://github.com/googleapis/google-api-dotnet-client/issues/2603
GC . Collect ( ) ;
}
// Wait for all processing to finish.
await Task . WhenAll ( batchProcessCallbacks ) . ConfigureAwait ( false ) ;
}
/// <summary>
/// Processes the delta changes for the given history changes.
/// Message downloads are not handled here since it's better to batch them.
/// </summary>
/// <param name="listHistoryResponse">List of history changes.</param>
private async Task ProcessHistoryChangesAsync ( ListHistoryResponse listHistoryResponse )
{
_logger . Debug ( "Processing delta change {HistoryId} for {Name}" , Account . Name , listHistoryResponse . HistoryId . GetValueOrDefault ( ) ) ;
foreach ( var history in listHistoryResponse . History )
{
// Handle label additions.
if ( history . LabelsAdded is not null )
{
foreach ( var addedLabel in history . LabelsAdded )
{
await HandleLabelAssignmentAsync ( addedLabel ) ;
}
}
// Handle label removals.
if ( history . LabelsRemoved is not null )
{
foreach ( var removedLabel in history . LabelsRemoved )
{
await HandleLabelRemovalAsync ( removedLabel ) ;
}
}
// Handle removed messages.
if ( history . MessagesDeleted is not null )
{
foreach ( var deletedMessage in history . MessagesDeleted )
{
var messageId = deletedMessage . Message . Id ;
_logger . Debug ( "Processing message deletion for {MessageId}" , messageId ) ;
await _gmailChangeProcessor . DeleteMailAsync ( Account . Id , messageId ) . ConfigureAwait ( false ) ;
}
}
}
}
private async Task HandleLabelAssignmentAsync ( HistoryLabelAdded addedLabel )
{
var messageId = addedLabel . Message . Id ;
_logger . Debug ( "Processing label assignment for message {MessageId}" , messageId ) ;
foreach ( var labelId in addedLabel . LabelIds )
{
// When UNREAD label is added mark the message as un-read.
2024-11-30 23:05:07 +01:00
if ( labelId = = ServiceConstants . UNREAD_LABEL_ID )
2024-04-18 01:44:37 +02:00
await _gmailChangeProcessor . ChangeMailReadStatusAsync ( messageId , false ) . ConfigureAwait ( false ) ;
// When STARRED label is added mark the message as flagged.
2024-11-30 23:05:07 +01:00
if ( labelId = = ServiceConstants . STARRED_LABEL_ID )
2024-04-18 01:44:37 +02:00
await _gmailChangeProcessor . ChangeFlagStatusAsync ( messageId , true ) . ConfigureAwait ( false ) ;
await _gmailChangeProcessor . CreateAssignmentAsync ( Account . Id , messageId , labelId ) . ConfigureAwait ( false ) ;
}
}
private async Task HandleLabelRemovalAsync ( HistoryLabelRemoved removedLabel )
{
var messageId = removedLabel . Message . Id ;
_logger . Debug ( "Processing label removed for message {MessageId}" , messageId ) ;
foreach ( var labelId in removedLabel . LabelIds )
{
// When UNREAD label is removed mark the message as read.
2024-11-30 23:05:07 +01:00
if ( labelId = = ServiceConstants . UNREAD_LABEL_ID )
2024-04-18 01:44:37 +02:00
await _gmailChangeProcessor . ChangeMailReadStatusAsync ( messageId , true ) . ConfigureAwait ( false ) ;
// When STARRED label is removed mark the message as un-flagged.
2024-11-30 23:05:07 +01:00
if ( labelId = = ServiceConstants . STARRED_LABEL_ID )
2024-04-18 01:44:37 +02:00
await _gmailChangeProcessor . ChangeFlagStatusAsync ( messageId , false ) . ConfigureAwait ( false ) ;
// For other labels remove the mail assignment.
await _gmailChangeProcessor . DeleteAssignmentAsync ( Account . Id , messageId , labelId ) . ConfigureAwait ( false ) ;
}
}
/// <summary>
/// Prepares Gmail Draft object from Google SDK.
/// If provided, ThreadId ties the draft to a thread. Used when replying messages.
/// If provided, DraftId updates the draft instead of creating a new one.
/// </summary>
/// <param name="mimeMessage">MailKit MimeMessage to include as raw message into Gmail request.</param>
/// <param name="messageThreadId">ThreadId that this draft should be tied to.</param>
/// <param name="messageDraftId">Existing DraftId from Gmail to update existing draft.</param>
/// <returns></returns>
private Draft PrepareGmailDraft ( MimeMessage mimeMessage , string messageThreadId = "" , string messageDraftId = "" )
{
mimeMessage . Prepare ( EncodingConstraint . None ) ;
var mimeString = mimeMessage . ToString ( ) ;
var base64UrlEncodedMime = Base64UrlEncoder . Encode ( mimeString ) ;
var nativeMessage = new Message ( )
{
Raw = base64UrlEncodedMime ,
} ;
if ( ! string . IsNullOrEmpty ( messageThreadId ) )
nativeMessage . ThreadId = messageThreadId ;
var draft = new Draft ( )
{
Message = nativeMessage ,
Id = messageDraftId
} ;
return draft ;
}
#region Mail Integrations
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < IClientServiceRequest > > Move ( BatchMoveRequest request )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
var toFolder = request [ 0 ] . ToFolder ;
var fromFolder = request [ 0 ] . FromFolder ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
// Sent label can't be removed from mails for Gmail.
// They are automatically assigned by Gmail.
// When you delete sent mail from gmail web portal, it's moved to Trash
// but still has Sent label. It's just hidden from the user.
// Proper assignments will be done later on CreateAssignment call to mimic this behavior.
2024-08-29 23:58:39 +02:00
2024-11-26 20:03:10 +01:00
var batchModifyRequest = new BatchModifyMessagesRequest
{
Ids = request . Select ( a = > a . Item . Id . ToString ( ) ) . ToList ( ) ,
AddLabelIds = [ toFolder . RemoteFolderId ]
} ;
// Only add remove label ids if the source folder is not sent folder.
if ( fromFolder . SpecialFolderType ! = SpecialFolderType . Sent )
{
batchModifyRequest . RemoveLabelIds = [ fromFolder . RemoteFolderId ] ;
}
var networkCall = _gmailService . Users . Messages . BatchModify ( batchModifyRequest , "me" ) ;
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , request ) ] ;
2024-04-18 01:44:37 +02:00
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < IClientServiceRequest > > ChangeFlag ( BatchChangeFlagRequest request )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
bool isFlagged = request [ 0 ] . IsFlagged ;
var batchModifyRequest = new BatchModifyMessagesRequest
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
Ids = request . Select ( a = > a . Item . Id . ToString ( ) ) . ToList ( ) ,
} ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
if ( isFlagged )
2024-11-30 23:05:07 +01:00
batchModifyRequest . AddLabelIds = new List < string > ( ) { ServiceConstants . STARRED_LABEL_ID } ;
2024-11-26 20:03:10 +01:00
else
2024-11-30 23:05:07 +01:00
batchModifyRequest . RemoveLabelIds = new List < string > ( ) { ServiceConstants . STARRED_LABEL_ID } ;
2024-11-26 20:03:10 +01:00
var networkCall = _gmailService . Users . Messages . BatchModify ( batchModifyRequest , "me" ) ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , request ) ] ;
2024-04-18 01:44:37 +02:00
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < IClientServiceRequest > > MarkRead ( BatchMarkReadRequest request )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
bool readStatus = request [ 0 ] . IsRead ;
var batchModifyRequest = new BatchModifyMessagesRequest
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
Ids = request . Select ( a = > a . Item . Id . ToString ( ) ) . ToList ( ) ,
} ;
if ( readStatus )
2024-11-30 23:05:07 +01:00
batchModifyRequest . RemoveLabelIds = new List < string > ( ) { ServiceConstants . UNREAD_LABEL_ID } ;
2024-11-26 20:03:10 +01:00
else
2024-11-30 23:05:07 +01:00
batchModifyRequest . AddLabelIds = new List < string > ( ) { ServiceConstants . UNREAD_LABEL_ID } ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
var networkCall = _gmailService . Users . Messages . BatchModify ( batchModifyRequest , "me" ) ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , request ) ] ;
2024-04-18 01:44:37 +02:00
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < IClientServiceRequest > > Delete ( BatchDeleteRequest request )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
var batchModifyRequest = new BatchDeleteMessagesRequest
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
Ids = request . Select ( a = > a . Item . Id . ToString ( ) ) . ToList ( ) ,
} ;
var networkCall = _gmailService . Users . Messages . BatchDelete ( batchModifyRequest , "me" ) ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , request ) ] ;
2024-04-18 01:44:37 +02:00
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < IClientServiceRequest > > CreateDraft ( CreateDraftRequest singleRequest )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
Draft draft = null ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
// It's new mail. Not a reply
if ( singleRequest . DraftPreperationRequest . ReferenceMailCopy = = null )
draft = PrepareGmailDraft ( singleRequest . DraftPreperationRequest . CreatedLocalDraftMimeMessage ) ;
else
draft = PrepareGmailDraft ( singleRequest . DraftPreperationRequest . CreatedLocalDraftMimeMessage ,
singleRequest . DraftPreperationRequest . ReferenceMailCopy . ThreadId ,
singleRequest . DraftPreperationRequest . ReferenceMailCopy . DraftId ) ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
var networkCall = _gmailService . Users . Drafts . Create ( draft , "me" ) ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , singleRequest , singleRequest ) ] ;
2024-04-18 01:44:37 +02:00
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < IClientServiceRequest > > Archive ( BatchArchiveRequest request )
2024-06-21 23:48:03 +02:00
{
2024-11-26 20:03:10 +01:00
bool isArchiving = request [ 0 ] . IsArchiving ;
var batchModifyRequest = new BatchModifyMessagesRequest
2024-06-21 23:48:03 +02:00
{
2024-11-26 20:03:10 +01:00
Ids = request . Select ( a = > a . Item . Id . ToString ( ) ) . ToList ( )
} ;
2024-06-21 23:48:03 +02:00
2024-11-26 20:03:10 +01:00
if ( isArchiving )
{
2024-11-30 23:05:07 +01:00
batchModifyRequest . RemoveLabelIds = new [ ] { ServiceConstants . INBOX_LABEL_ID } ;
2024-11-26 20:03:10 +01:00
}
else
{
2024-11-30 23:05:07 +01:00
batchModifyRequest . AddLabelIds = new [ ] { ServiceConstants . INBOX_LABEL_ID } ;
2024-11-26 20:03:10 +01:00
}
2024-06-21 23:48:03 +02:00
2024-11-26 20:03:10 +01:00
var networkCall = _gmailService . Users . Messages . BatchModify ( batchModifyRequest , "me" ) ;
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , request ) ] ;
2024-06-21 23:48:03 +02:00
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < IClientServiceRequest > > SendDraft ( SendDraftRequest singleDraftRequest )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
var message = new Message ( ) ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
if ( ! string . IsNullOrEmpty ( singleDraftRequest . Item . ThreadId ) )
{
message . ThreadId = singleDraftRequest . Item . ThreadId ;
}
2024-06-14 00:39:18 +02:00
2024-11-26 20:03:10 +01:00
singleDraftRequest . Request . Mime . Prepare ( EncodingConstraint . None ) ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
var mimeString = singleDraftRequest . Request . Mime . ToString ( ) ;
var base64UrlEncodedMime = Base64UrlEncoder . Encode ( mimeString ) ;
message . Raw = base64UrlEncodedMime ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
var draft = new Draft ( )
{
Id = singleDraftRequest . Request . MailItem . DraftId ,
Message = message
} ;
var networkCall = _gmailService . Users . Drafts . Send ( draft , "me" ) ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , singleDraftRequest , singleDraftRequest ) ] ;
2024-04-18 01:44:37 +02:00
}
public override async Task DownloadMissingMimeMessageAsync ( IMailItem mailItem ,
ITransferProgress transferProgress = null ,
CancellationToken cancellationToken = default )
{
2024-06-12 00:32:04 +02:00
var request = _gmailService . Users . Messages . Get ( "me" , mailItem . Id ) ;
request . Format = UsersResource . MessagesResource . GetRequest . FormatEnum . Raw ;
var gmailMessage = await request . ExecuteAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
var mimeMessage = gmailMessage . GetGmailMimeMessage ( ) ;
2024-06-12 00:32:04 +02:00
if ( mimeMessage = = null )
{
_logger . Warning ( "Tried to download Gmail Raw Mime with {Id} id and server responded without a data." , mailItem . Id ) ;
return ;
}
2024-04-18 01:44:37 +02:00
await _gmailChangeProcessor . SaveMimeFileAsync ( mailItem . FileId , mimeMessage , Account . Id ) . ConfigureAwait ( false ) ;
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < IClientServiceRequest > > RenameFolder ( RenameFolderRequest request )
2024-07-09 01:05:16 +02:00
{
2024-11-26 20:03:10 +01:00
var label = new Label ( )
2024-07-09 01:05:16 +02:00
{
2024-11-26 20:03:10 +01:00
Name = request . NewFolderName
} ;
2024-07-09 01:05:16 +02:00
2024-11-26 20:03:10 +01:00
var networkCall = _gmailService . Users . Labels . Update ( label , "me" , request . Folder . RemoteFolderId ) ;
2024-07-09 01:05:16 +02:00
2024-11-26 20:03:10 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , request , request ) ] ;
2024-07-09 01:05:16 +02:00
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < IClientServiceRequest > > EmptyFolder ( EmptyFolderRequest request )
2024-07-09 01:05:16 +02:00
{
// Create batch delete request.
var deleteRequests = request . MailsToDelete . Select ( a = > new DeleteRequest ( a ) ) ;
return Delete ( new BatchDeleteRequest ( deleteRequests ) ) ;
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < IClientServiceRequest > > MarkFolderAsRead ( MarkFolderAsReadRequest request )
= > MarkRead ( new BatchMarkReadRequest ( request . MailsToMarkRead . Select ( a = > new MarkReadRequest ( a , true ) ) ) ) ;
2024-07-09 01:05:16 +02:00
2024-04-18 01:44:37 +02:00
#endregion
#region Request Execution
2024-11-26 20:03:10 +01:00
public override async Task ExecuteNativeRequestsAsync ( List < IRequestBundle < IClientServiceRequest > > batchedRequests ,
2024-04-18 01:44:37 +02:00
CancellationToken cancellationToken = default )
{
var batchedBundles = batchedRequests . Batch ( ( int ) MaximumAllowedBatchRequestSize ) ;
var bundleCount = batchedBundles . Count ( ) ;
for ( int i = 0 ; i < bundleCount ; i + + )
{
var bundle = batchedBundles . ElementAt ( i ) ;
var nativeBatchRequest = new BatchRequest ( _gmailService ) ;
var bundleRequestCount = bundle . Count ( ) ;
2024-08-29 22:43:27 +02:00
var bundleTasks = new List < Task > ( ) ;
2024-04-18 01:44:37 +02:00
for ( int k = 0 ; k < bundleRequestCount ; k + + )
{
var requestBundle = bundle . ElementAt ( k ) ;
2024-11-26 20:03:10 +01:00
requestBundle . UIChangeRequest ? . ApplyUIChanges ( ) ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
nativeBatchRequest . Queue < object > ( requestBundle . NativeRequest , ( content , error , index , message )
2024-08-29 22:43:27 +02:00
= > bundleTasks . Add ( ProcessSingleNativeRequestResponseAsync ( requestBundle , error , message , cancellationToken ) ) ) ;
2024-04-18 01:44:37 +02:00
}
await nativeBatchRequest . ExecuteAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2024-08-29 22:43:27 +02:00
await Task . WhenAll ( bundleTasks ) ;
2024-04-18 01:44:37 +02:00
}
}
2024-08-29 22:43:27 +02:00
private void ProcessGmailRequestError ( RequestError error , IRequestBundle < IClientServiceRequest > bundle )
2024-04-18 01:44:37 +02:00
{
if ( error = = null ) return ;
// OutOfMemoryException is a known bug in Gmail SDK.
if ( error . Code = = 0 )
{
2024-11-26 20:03:10 +01:00
bundle ? . UIChangeRequest ? . RevertUIChanges ( ) ;
2024-04-18 01:44:37 +02:00
throw new OutOfMemoryException ( error . Message ) ;
}
// Entity not found.
if ( error . Code = = 404 )
{
2024-11-26 20:03:10 +01:00
bundle ? . UIChangeRequest ? . RevertUIChanges ( ) ;
2024-04-18 01:44:37 +02:00
throw new SynchronizerEntityNotFoundException ( error . Message ) ;
}
if ( ! string . IsNullOrEmpty ( error . Message ) )
{
2024-11-26 20:03:10 +01:00
bundle ? . UIChangeRequest ? . RevertUIChanges ( ) ;
2024-04-18 01:44:37 +02:00
error . Errors ? . ForEach ( error = > _logger . Error ( "Unknown Gmail SDK error for {Name}\n{Error}" , Account . Name , error ) ) ;
2024-08-29 22:43:27 +02:00
throw new SynchronizerException ( error . Message ) ;
2024-04-18 01:44:37 +02:00
}
}
/// <summary>
/// Handles after each single message download.
/// This involves adding the Gmail message into Wino database.
/// </summary>
/// <param name="message"></param>
/// <param name="error"></param>
/// <param name="httpResponseMessage"></param>
/// <param name="cancellationToken"></param>
private async Task HandleSingleItemDownloadedCallbackAsync ( Message message ,
RequestError error ,
string downloadingMessageId ,
CancellationToken cancellationToken = default )
{
try
{
2024-08-29 22:43:27 +02:00
ProcessGmailRequestError ( error , null ) ;
2024-04-18 01:44:37 +02:00
}
catch ( OutOfMemoryException )
{
_logger . Warning ( "Gmail SDK got OutOfMemoryException due to bug in the SDK" ) ;
}
catch ( SynchronizerEntityNotFoundException )
{
_logger . Warning ( "Resource not found for {DownloadingMessageId}" , downloadingMessageId ) ;
}
catch ( SynchronizerException synchronizerException )
{
_logger . Error ( "Gmail SDK returned error for {DownloadingMessageId}\n{SynchronizerException}" , downloadingMessageId , synchronizerException ) ;
}
if ( message = = null )
{
_logger . Warning ( "Skipped GMail message download for {DownloadingMessageId}" , downloadingMessageId ) ;
return ;
}
// Gmail has LabelId property for each message.
// Therefore we can pass null as the assigned folder safely.
var mailPackage = await CreateNewMailPackagesAsync ( message , null , cancellationToken ) ;
// If CreateNewMailPackagesAsync returns null it means local draft mapping is done.
// We don't need to insert anything else.
if ( mailPackage = = null )
return ;
foreach ( var package in mailPackage )
{
await _gmailChangeProcessor . CreateMailAsync ( Account . Id , package ) . ConfigureAwait ( false ) ;
}
// Try updating the history change identifier if any.
if ( message . HistoryId = = null ) return ;
// Delta changes also has history id but the maximum id is preserved in the account service.
// TODO: This is not good. Centralize the identifier fetch and prevent direct access here.
Account . SynchronizationDeltaIdentifier = await _gmailChangeProcessor . UpdateAccountDeltaSynchronizationIdentifierAsync ( Account . Id , message . HistoryId . ToString ( ) ) ;
}
private async Task ProcessSingleNativeRequestResponseAsync ( IRequestBundle < IClientServiceRequest > bundle ,
RequestError error ,
HttpResponseMessage httpResponseMessage ,
CancellationToken cancellationToken = default )
{
2024-08-29 22:43:27 +02:00
ProcessGmailRequestError ( error , bundle ) ;
2024-04-18 01:44:37 +02:00
if ( bundle is HttpRequestBundle < IClientServiceRequest , Message > messageBundle )
{
var gmailMessage = await messageBundle . DeserializeBundleAsync ( httpResponseMessage , cancellationToken ) . ConfigureAwait ( false ) ;
if ( gmailMessage = = null ) return ;
await HandleSingleItemDownloadedCallbackAsync ( gmailMessage , error , "unknown" , cancellationToken ) ;
}
else if ( bundle is HttpRequestBundle < IClientServiceRequest , Label > folderBundle )
{
var gmailLabel = await folderBundle . DeserializeBundleAsync ( httpResponseMessage , cancellationToken ) . ConfigureAwait ( false ) ;
if ( gmailLabel = = null ) return ;
// TODO: Handle new Gmail Label added or updated.
}
else if ( bundle is HttpRequestBundle < IClientServiceRequest , Draft > draftBundle & & draftBundle . Request is CreateDraftRequest createDraftRequest )
{
// New draft mail is created.
var messageDraft = await draftBundle . DeserializeBundleAsync ( httpResponseMessage , cancellationToken ) . ConfigureAwait ( false ) ;
if ( messageDraft = = null ) return ;
var localDraftCopy = createDraftRequest . DraftPreperationRequest . CreatedLocalDraftCopy ;
// Here we have DraftId, MessageId and ThreadId.
// Update the local copy properties and re-synchronize to get the original message and update history.
// We don't fetch the single message here because it may skip some of the history changes when the
// fetch updates the historyId. Therefore we need to re-synchronize to get the latest history changes
// which will have the original message downloaded eventually.
await _gmailChangeProcessor . MapLocalDraftAsync ( Account . Id , localDraftCopy . UniqueId , messageDraft . Message . Id , messageDraft . Id , messageDraft . Message . ThreadId ) ;
2024-12-24 18:30:25 +01:00
var options = new MailSynchronizationOptions ( )
2024-04-18 01:44:37 +02:00
{
AccountId = Account . Id ,
2024-12-24 18:30:25 +01:00
Type = MailSynchronizationType . FullFolders
2024-04-18 01:44:37 +02:00
} ;
2024-12-24 18:30:25 +01:00
await SynchronizeMailsInternalAsync ( options , cancellationToken ) ;
2024-04-18 01:44:37 +02:00
}
}
/// <summary>
/// Maps existing Gmail Draft resources to local mail copies.
/// This uses indexed search, therefore it's quite fast.
/// It's safe to execute this after each Draft creation + batch message download.
/// </summary>
private async Task MapDraftIdsAsync ( CancellationToken cancellationToken = default )
{
// TODO: This call is not necessary if we don't have any local drafts.
// Remote drafts will be downloaded in missing message batches anyways.
// Fix it by checking whether we need to do this or not.
var drafts = await _gmailService . Users . Drafts . List ( "me" ) . ExecuteAsync ( cancellationToken ) ;
if ( drafts . Drafts = = null )
{
_logger . Information ( "There are no drafts to map for {Name}" , Account . Name ) ;
return ;
}
foreach ( var draft in drafts . Drafts )
{
await _gmailChangeProcessor . MapLocalDraftAsync ( draft . Message . Id , draft . Id , draft . Message . ThreadId ) ;
}
}
/// <summary>
/// Creates new mail packages for the given message.
/// AssignedFolder is null since the LabelId is parsed out of the Message.
/// </summary>
/// <param name="message">Gmail message to create package for.</param>
/// <param name="assignedFolder">Null, not used.</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>New mail package that change processor can use to insert new mail into database.</returns>
public override async Task < List < NewMailItemPackage > > CreateNewMailPackagesAsync ( Message message ,
MailItemFolder assignedFolder ,
CancellationToken cancellationToken = default )
{
var packageList = new List < NewMailItemPackage > ( ) ;
MimeMessage mimeMessage = message . GetGmailMimeMessage ( ) ;
var mailCopy = message . AsMailCopy ( mimeMessage ) ;
// Check whether this message is mapped to any local draft.
// Previously we were using Draft resource response as mapping drafts.
// This seem to be a worse approach. Now both Outlook and Gmail use X-Wino-Draft-Id header to map drafts.
// This is a better approach since we don't need to fetch the draft resource to get the draft id.
2024-06-12 02:12:23 +02:00
if ( mailCopy . IsDraft
& & mimeMessage . Headers . Contains ( Domain . Constants . WinoLocalDraftHeader )
2024-04-18 01:44:37 +02:00
& & Guid . TryParse ( mimeMessage . Headers [ Domain . Constants . WinoLocalDraftHeader ] , out Guid localDraftCopyUniqueId ) )
{
// This message belongs to existing local draft copy.
// We don't need to create a new mail copy for this message, just update the existing one.
bool isMappingSuccesfull = await _gmailChangeProcessor . MapLocalDraftAsync ( Account . Id , localDraftCopyUniqueId , mailCopy . Id , mailCopy . DraftId , mailCopy . ThreadId ) ;
if ( isMappingSuccesfull ) return null ;
// Local copy doesn't exists. Continue execution to insert mail copy.
}
2024-04-18 04:14:37 +03:00
if ( message . LabelIds is not null )
2024-04-18 01:44:37 +02:00
{
2024-04-18 04:14:37 +03:00
foreach ( var labelId in message . LabelIds )
{
packageList . Add ( new NewMailItemPackage ( mailCopy , mimeMessage , labelId ) ) ;
}
2024-04-18 01:44:37 +02:00
}
return packageList ;
}
#endregion
2025-02-15 12:53:32 +01:00
public override async Task KillSynchronizerAsync ( )
{
await base . KillSynchronizerAsync ( ) ;
_gmailService . Dispose ( ) ;
_peopleService . Dispose ( ) ;
_calendarService . Dispose ( ) ;
_googleHttpClient . Dispose ( ) ;
}
2024-04-18 01:44:37 +02:00
}
}