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 ;
2025-02-22 17:51:38 +01:00
using Google ;
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 ;
2025-02-22 00:22:00 +01:00
using Wino.Core.Domain.Models.Folders ;
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
2025-02-16 11:54:23 +01:00
namespace Wino.Core.Synchronizers.Mail ;
public class GmailSynchronizer : WinoSynchronizer < IClientServiceRequest , Message , Event > , IHttpClientFactory
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
public override uint BatchModificationSize = > 1000 ;
2025-02-22 23:09:53 +01:00
/// <summary>
/// This is NOT the initial message download count per folder.
/// Gmail doesn't have per-folder sync. Therefore this represents to total amount that 1 page query returns until
/// there are no pages to get. Max allowed is 500.
/// </summary>
public override uint InitialMessageDownloadCountPerFolder = > 500 ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// 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-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
private readonly ConfigurableHttpClient _googleHttpClient ;
private readonly GmailService _gmailService ;
private readonly CalendarService _calendarService ;
private readonly PeopleServiceService _peopleService ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
private readonly IGmailChangeProcessor _gmailChangeProcessor ;
private readonly ILogger _logger = Log . ForContext < GmailSynchronizer > ( ) ;
2024-08-15 23:57:45 +02:00
2025-02-23 17:05:46 +01:00
// Keeping a reference for quick access to the virtual archive folder.
private Guid ? archiveFolderId ;
2025-02-16 11:54:23 +01:00
public GmailSynchronizer ( MailAccount account ,
IGmailAuthenticator authenticator ,
IGmailChangeProcessor gmailChangeProcessor ) : base ( account )
{
var messageHandler = new GmailClientMessageHandler ( authenticator , account ) ;
2024-08-15 23:57:45 +02:00
2025-02-16 11:54:23 +01:00
var initializer = new BaseClientService . Initializer ( )
{
HttpClientFactory = this
} ;
2024-08-15 23:57:45 +02:00
2025-02-16 11:54:23 +01:00
_googleHttpClient = new ConfigurableHttpClient ( messageHandler ) ;
_gmailService = new GmailService ( initializer ) ;
_peopleService = new PeopleServiceService ( initializer ) ;
_calendarService = new CalendarService ( initializer ) ;
2024-08-15 23:57:45 +02:00
2025-02-16 11:54:23 +01:00
_gmailChangeProcessor = gmailChangeProcessor ;
}
2024-08-15 23:57:45 +02:00
2025-02-16 11:54:23 +01:00
public ConfigurableHttpClient CreateHttpClient ( CreateHttpClientArgs args ) = > _googleHttpClient ;
2024-08-15 23:57:45 +02:00
2025-02-16 11:54:23 +01:00
public override async Task < ProfileInformation > GetProfileInformationAsync ( )
{
var profileRequest = _peopleService . People . Get ( "people/me" ) ;
profileRequest . PersonFields = "names,photos,emailAddresses" ;
2024-08-15 23:57:45 +02:00
2025-02-16 11:54:23 +01:00
string senderName = string . Empty , base64ProfilePicture = string . Empty , address = string . Empty ;
2024-12-01 03:05:15 +01:00
2025-02-16 11:54:23 +01:00
var userProfile = await profileRequest . ExecuteAsync ( ) ;
2024-08-16 01:29:31 +02:00
2025-02-16 11:54:23 +01:00
senderName = userProfile . Names ? . FirstOrDefault ( ) ? . DisplayName ? ? Account . SenderName ;
2024-08-16 01:29:31 +02:00
2025-02-16 11:54:23 +01:00
var profilePicture = userProfile . Photos ? . FirstOrDefault ( ) ? . Url ? ? string . Empty ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( ! string . IsNullOrEmpty ( profilePicture ) )
{
base64ProfilePicture = await GetProfilePictureBase64EncodedAsync ( profilePicture ) . ConfigureAwait ( false ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
address = userProfile . EmailAddresses . FirstOrDefault ( a = > a . Metadata . Primary = = true ) . Value ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return new ProfileInformation ( senderName , base64ProfilePicture , address ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
protected override async Task SynchronizeAliasesAsync ( )
{
var sendAsListRequest = _gmailService . Users . Settings . SendAs . List ( "me" ) ;
var sendAsListResponse = await sendAsListRequest . ExecuteAsync ( ) ;
var remoteAliases = sendAsListResponse . GetRemoteAliases ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await _gmailChangeProcessor . UpdateRemoteAliasInformationAsync ( Account , remoteAliases ) . ConfigureAwait ( false ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
protected override async Task < MailSynchronizationResult > SynchronizeMailsInternalAsync ( MailSynchronizationOptions options , CancellationToken cancellationToken = default )
{
_logger . Information ( "Internal mail synchronization started for {Name}" , Account . Name ) ;
2024-04-18 01:44:37 +02:00
2025-02-23 17:05:46 +01:00
// Make sure that virtual archive folder exists before all.
if ( ! archiveFolderId . HasValue )
await InitializeArchiveFolderAsync ( ) . ConfigureAwait ( false ) ;
2025-02-16 11:54:23 +01:00
// Gmail must always synchronize folders before because it doesn't have a per-folder sync.
bool shouldSynchronizeFolders = true ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( shouldSynchronizeFolders )
{
_logger . Information ( "Synchronizing folders for {Name}" , Account . Name ) ;
2024-04-18 01:44:37 +02:00
2025-02-22 17:51:38 +01:00
try
{
await SynchronizeFoldersAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
}
catch ( GoogleApiException googleException ) when ( googleException . Message . Contains ( "Mail service not enabled" ) )
{
throw new GmailServiceDisabledException ( ) ;
}
catch ( Exception )
{
throw ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
_logger . Information ( "Synchronizing folders for {Name} is completed" , Account . Name ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// 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-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( options . Type = = MailSynchronizationType . FoldersOnly ) return MailSynchronizationResult . Empty ;
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
retry :
2025-02-16 11:54:23 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
bool isInitialSync = string . IsNullOrEmpty ( Account . SynchronizationDeltaIdentifier ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
_logger . Debug ( "Is initial synchronization: {IsInitialSync}" , isInitialSync ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var missingMessageIds = new List < string > ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var deltaChanges = new List < ListHistoryResponse > ( ) ; // For tracking delta changes.
var listChanges = new List < ListMessagesResponse > ( ) ; // For tracking initial sync changes.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
/ * 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 .
* /
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
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.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var messageRequest = _gmailService . Users . Messages . List ( "me" ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// 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 ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
ListMessagesResponse result = null ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
string nextPageToken = string . Empty ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
while ( true )
{
if ( ! string . IsNullOrEmpty ( nextPageToken ) )
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
messageRequest . PageToken = nextPageToken ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
result = await messageRequest . ExecuteAsync ( cancellationToken ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
nextPageToken = result . NextPageToken ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
listChanges . Add ( result ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Nothing to fetch anymore. Break the loop.
if ( nextPageToken = = null )
break ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
}
else
{
var startHistoryId = ulong . Parse ( Account . SynchronizationDeltaIdentifier ) ;
var nextPageToken = ulong . Parse ( Account . SynchronizationDeltaIdentifier ) . ToString ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var historyRequest = _gmailService . Users . History . List ( "me" ) ;
historyRequest . StartHistoryId = startHistoryId ;
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
try
2025-02-16 11:54:23 +01:00
{
2025-02-22 23:09:53 +01:00
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 ;
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
var historyResponse = await historyRequest . ExecuteAsync ( cancellationToken ) ;
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
nextPageToken = historyResponse . NextPageToken ;
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
if ( historyResponse . History = = null )
continue ;
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
deltaChanges . Add ( historyResponse ) ;
}
}
catch ( GoogleApiException ex ) when ( ex . HttpStatusCode = = System . Net . HttpStatusCode . NotFound )
{
// History ID is too old or expired, need to do a full sync.
// Theoratically we need to delete the local cache and start from scratch.
_logger . Warning ( "History ID {StartHistoryId} is expired for {Name}. Will remove user's mail cache and do full sync." , startHistoryId , Account . Name ) ;
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
await _gmailChangeProcessor . DeleteUserMailCacheAsync ( Account . Id ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
Account . SynchronizationDeltaIdentifier = string . Empty ;
await _gmailChangeProcessor . UpdateAccountAsync ( Account ) . ConfigureAwait ( false ) ;
goto retry ;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
// Add initial message ids from initial sync.
missingMessageIds . AddRange ( listChanges . Where ( a = > a . Messages ! = null ) . SelectMany ( a = > a . Messages ) . Select ( a = > a . Id ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// 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 ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
missingMessageIds . AddRange ( addedMessageIds ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-23 17:05:46 +01:00
// Start downloading missing messages.
await BatchDownloadMessagesAsync ( missingMessageIds , cancellationToken ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-23 17:05:46 +01:00
// Map archive assignments if there are any changes reported.
if ( listChanges . Any ( ) | | deltaChanges . Any ( ) )
2025-02-16 11:54:23 +01:00
{
2025-02-23 17:05:46 +01:00
await MapArchivedMailsAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Map remote drafts to local drafts.
await MapDraftIdsAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2024-12-24 18:30:25 +01:00
2025-02-16 11:54:23 +01:00
// Start processing delta changes.
foreach ( var historyResponse in deltaChanges )
{
await ProcessHistoryChangesAsync ( historyResponse ) . ConfigureAwait ( false ) ;
}
2024-12-24 18:30:25 +01:00
2025-02-16 11:54:23 +01:00
// Take the max history id from delta changes and update the account sync modifier.
var maxHistoryId = deltaChanges . Max ( a = > a . HistoryId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
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 ) ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
_logger . Debug ( "Final sync identifier {SynchronizationDeltaIdentifier}" , Account . SynchronizationDeltaIdentifier ) ;
}
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
// Get all unred new downloaded items and return in the result.
// This is primarily used in notifications.
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
var unreadNewItems = await _gmailChangeProcessor . GetDownloadedUnreadMailsAsync ( Account . Id , missingMessageIds ) . ConfigureAwait ( false ) ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
return MailSynchronizationResult . Completed ( unreadNewItems ) ;
}
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
protected override async Task < CalendarSynchronizationResult > SynchronizeCalendarEventsInternalAsync ( CalendarSynchronizationOptions options , CancellationToken cancellationToken = default )
{
_logger . Information ( "Internal calendar synchronization started for {Name}" , Account . Name ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await SynchronizeCalendarsAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
bool isInitialSync = string . IsNullOrEmpty ( Account . SynchronizationDeltaIdentifier ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
_logger . Debug ( "Is initial synchronization: {IsInitialSync}" , isInitialSync ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var localCalendars = await _gmailChangeProcessor . GetAccountCalendarsAsync ( Account . Id ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// TODO: Better logging and exception handling.
foreach ( var calendar in localCalendars )
{
var request = _calendarService . Events . List ( calendar . RemoteCalendarId ) ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
request . SingleEvents = false ;
request . ShowDeleted = true ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
if ( ! string . IsNullOrEmpty ( calendar . SynchronizationDeltaToken ) )
{
// If a sync token is available, perform an incremental sync
request . SyncToken = calendar . SynchronizationDeltaToken ;
}
else
{
// If no sync token, perform an initial sync
// Fetch events from the past year
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
request . TimeMinDateTimeOffset = DateTimeOffset . UtcNow . AddYears ( - 1 ) ;
}
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
string nextPageToken ;
string syncToken ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
var allEvents = new List < Event > ( ) ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
do
{
// Execute the request
var events = await request . ExecuteAsync ( ) ;
2025-01-04 11:39:32 +01:00
2025-02-16 11:54:23 +01:00
// Process the fetched events
if ( events . Items ! = null )
{
allEvents . AddRange ( events . Items ) ;
}
2025-01-04 11:39:32 +01:00
2025-02-16 11:54:23 +01:00
// Get the next page token and sync token
nextPageToken = events . NextPageToken ;
syncToken = events . NextSyncToken ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
// Set the next page token for subsequent requests
request . PageToken = nextPageToken ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
} while ( ! string . IsNullOrEmpty ( nextPageToken ) ) ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
calendar . SynchronizationDeltaToken = syncToken ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
// allEvents contains new or updated events.
// Process them and create/update local calendar items.
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
foreach ( var @event in allEvents )
{
// TODO: Exception handling for event processing.
// TODO: Also update attendees and other properties.
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
await _gmailChangeProcessor . ManageCalendarEventAsync ( @event , calendar , Account ) . ConfigureAwait ( false ) ;
2025-02-16 11:43:30 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await _gmailChangeProcessor . UpdateAccountCalendarAsync ( calendar ) . ConfigureAwait ( false ) ;
2025-02-16 11:43:30 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return default ;
}
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
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 ;
}
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
var localCalendars = await _gmailChangeProcessor . GetAccountCalendarsAsync ( Account . Id ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
List < AccountCalendar > insertedCalendars = new ( ) ;
List < AccountCalendar > updatedCalendars = new ( ) ;
List < AccountCalendar > deletedCalendars = new ( ) ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
// 1. Handle deleted calendars.
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
foreach ( var calendar in localCalendars )
{
var remoteCalendar = calendarListResponse . Items . FirstOrDefault ( a = > a . Id = = calendar . RemoteCalendarId ) ;
if ( remoteCalendar = = null )
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
// Local calendar doesn't exists remotely. Delete local copy.
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
await _gmailChangeProcessor . DeleteAccountCalendarAsync ( calendar ) . ConfigureAwait ( false ) ;
deletedCalendars . Add ( calendar ) ;
2025-02-16 11:35:43 +01:00
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
// Delete the deleted folders from local list.
deletedCalendars . ForEach ( a = > localCalendars . Remove ( a ) ) ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +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
2024-12-27 00:18:46 +01:00
{
2025-02-16 11:54:23 +01:00
// Update existing calendar. Right now we only update the name.
if ( ShouldUpdateCalendar ( calendar , existingLocalCalendar ) )
2024-12-27 00:18:46 +01:00
{
2025-02-16 11:54:23 +01:00
existingLocalCalendar . Name = calendar . Summary ;
updatedCalendars . Add ( existingLocalCalendar ) ;
2024-12-27 00:18:46 +01:00
}
else
{
2025-02-16 11:54:23 +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
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +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
2025-02-16 11:54:23 +01:00
foreach ( var calendar in updatedCalendars )
{
await _gmailChangeProcessor . UpdateAccountCalendarAsync ( calendar ) . ConfigureAwait ( false ) ;
}
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
if ( insertedCalendars . Any ( ) | | deletedCalendars . Any ( ) | | updatedCalendars . Any ( ) )
{
// TODO: Notify calendar updates.
// WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id));
2024-12-27 00:18:46 +01:00
}
2025-02-16 11:54:23 +01:00
}
2025-02-23 17:05:46 +01:00
private async Task InitializeArchiveFolderAsync ( )
{
var localFolders = await _gmailChangeProcessor . GetLocalFoldersAsync ( Account . Id ) . ConfigureAwait ( false ) ;
// Handling of Gmail special virtual Archive folder.
// We will generate a new virtual folder if doesn't exist.
if ( ! localFolders . Any ( a = > a . SpecialFolderType = = SpecialFolderType . Archive ) )
{
archiveFolderId = Guid . NewGuid ( ) ;
var archiveFolder = new MailItemFolder ( )
{
FolderName = "Archive" , // will be localized. N/A
RemoteFolderId = ServiceConstants . ARCHIVE_LABEL_ID ,
Id = archiveFolderId . Value ,
MailAccountId = Account . Id ,
SpecialFolderType = SpecialFolderType . Archive ,
IsSynchronizationEnabled = true ,
IsSystemFolder = true ,
IsSticky = true ,
IsHidden = false ,
ShowUnreadCount = true
} ;
await _gmailChangeProcessor . InsertFolderAsync ( archiveFolder ) . ConfigureAwait ( false ) ;
// Migration-> User might've already have another special folder for Archive.
// We must remove that type assignment.
// This code can be removed after sometime.
var otherArchiveFolders = localFolders . Where ( a = > a . SpecialFolderType = = SpecialFolderType . Archive & & a . Id ! = archiveFolderId . Value ) . ToList ( ) ;
foreach ( var otherArchiveFolder in otherArchiveFolders )
{
otherArchiveFolder . SpecialFolderType = SpecialFolderType . Other ;
await _gmailChangeProcessor . UpdateFolderAsync ( otherArchiveFolder ) . ConfigureAwait ( false ) ;
}
}
else
{
archiveFolderId = localFolders . First ( a = > a . SpecialFolderType = = SpecialFolderType . Archive & & a . RemoteFolderId = = ServiceConstants . ARCHIVE_LABEL_ID ) . Id ;
}
}
2025-02-16 11:54:23 +01:00
private async Task SynchronizeFoldersAsync ( CancellationToken cancellationToken = default )
{
var localFolders = await _gmailChangeProcessor . GetLocalFoldersAsync ( Account . Id ) . ConfigureAwait ( false ) ;
var folderRequest = _gmailService . Users . Labels . List ( "me" ) ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
var labelsResponse = await folderRequest . ExecuteAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( labelsResponse . Labels = = null )
2024-12-27 00:18:46 +01:00
{
2025-02-16 11:54:23 +01:00
_logger . Warning ( "No folders found for {Name}" , Account . Name ) ;
return ;
}
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
List < MailItemFolder > insertedFolders = new ( ) ;
List < MailItemFolder > updatedFolders = new ( ) ;
List < MailItemFolder > deletedFolders = new ( ) ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
// 1. Handle deleted labels.
foreach ( var localFolder in localFolders )
{
// Category folder is virtual folder for Wino. Skip it.
if ( localFolder . SpecialFolderType = = SpecialFolderType . Category ) continue ;
2024-12-27 00:18:46 +01:00
2025-02-23 17:05:46 +01:00
// Gmail's Archive folder is virtual older for Wino. Skip it.
if ( localFolder . SpecialFolderType = = SpecialFolderType . Archive ) continue ;
2025-02-16 11:54:23 +01:00
var remoteFolder = labelsResponse . Labels . FirstOrDefault ( a = > a . Id = = localFolder . RemoteFolderId ) ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
if ( remoteFolder = = null )
2024-12-27 00:18:46 +01:00
{
2025-02-16 11:54:23 +01:00
// Local folder doesn't exists remotely. Delete local copy.
await _gmailChangeProcessor . DeleteFolderAsync ( Account . Id , localFolder . RemoteFolderId ) . ConfigureAwait ( false ) ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
deletedFolders . Add ( localFolder ) ;
}
}
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
// Delete the deleted folders from local list.
deletedFolders . ForEach ( a = > localFolders . Remove ( a ) ) ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
// 2. Handle update/insert based on remote folders.
foreach ( var remoteFolder in labelsResponse . Labels )
{
var existingLocalFolder = localFolders . FirstOrDefault ( a = > a . RemoteFolderId = = remoteFolder . Id ) ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
if ( existingLocalFolder = = null )
{
// Insert new folder.
var localFolder = remoteFolder . GetLocalFolder ( labelsResponse , Account . Id ) ;
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
insertedFolders . Add ( localFolder ) ;
}
else
2024-12-27 00:18:46 +01:00
{
2025-02-16 11:54:23 +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.
2024-07-09 01:05:16 +02:00
2025-02-16 11:54:23 +01:00
if ( ShouldUpdateFolder ( remoteFolder , existingLocalFolder ) )
2024-07-09 01:05:16 +02:00
{
2025-02-16 11:54:23 +01:00
existingLocalFolder . FolderName = remoteFolder . Name ;
existingLocalFolder . TextColorHex = remoteFolder . Color ? . TextColor ;
existingLocalFolder . BackgroundColorHex = remoteFolder . Color ? . BackgroundColor ;
2024-08-24 17:22:47 +02:00
2025-02-16 11:54:23 +01:00
updatedFolders . Add ( existingLocalFolder ) ;
2024-12-27 00:18:46 +01:00
}
else
2024-08-24 17:22:47 +02:00
{
2025-02-16 11:54:23 +01:00
// 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
}
2025-02-16 11:54:23 +01:00
}
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
// 3.Process changes in order-> Insert, Update. Deleted ones are already processed.
foreach ( var folder in insertedFolders )
{
await _gmailChangeProcessor . InsertFolderAsync ( folder ) . ConfigureAwait ( false ) ;
}
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
foreach ( var folder in updatedFolders )
{
await _gmailChangeProcessor . UpdateFolderAsync ( folder ) . ConfigureAwait ( false ) ;
2025-02-16 11:35:43 +01:00
}
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
if ( insertedFolders . Any ( ) | | deletedFolders . Any ( ) | | updatedFolders . Any ( ) )
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
WeakReferenceMessenger . Default . Send ( new AccountFolderConfigurationUpdated ( Account . Id ) ) ;
}
}
2024-12-27 00:18:46 +01:00
2025-02-16 11:54:23 +01:00
private bool ShouldUpdateCalendar ( CalendarListEntry calendarListEntry , AccountCalendar accountCalendar )
{
// TODO: Only calendar name is updated for now. We can add more checks here.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var remoteCalendarName = calendarListEntry . Summary ;
var localCalendarName = accountCalendar . Name ;
2024-08-24 17:22:47 +02:00
2025-02-16 11:54:23 +01:00
return ! localCalendarName . Equals ( remoteCalendarName , StringComparison . OrdinalIgnoreCase ) ;
}
2024-08-24 15:26:08 +02:00
2025-02-16 11:54:23 +01:00
private bool ShouldUpdateFolder ( Label remoteFolder , MailItemFolder existingLocalFolder )
{
var remoteFolderName = GoogleIntegratorExtensions . GetFolderName ( remoteFolder . Name ) ;
var localFolderName = GoogleIntegratorExtensions . GetFolderName ( existingLocalFolder . FolderName ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:54:23 +01:00
bool isNameChanged = ! localFolderName . Equals ( remoteFolderName , StringComparison . OrdinalIgnoreCase ) ;
bool isColorChanged = existingLocalFolder . BackgroundColorHex ! = remoteFolder . Color ? . BackgroundColor | |
existingLocalFolder . TextColorHex ! = remoteFolder . Color ? . TextColor ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return isNameChanged | | isColorChanged ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01: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 ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return singleRequest ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
/// <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>
private async Task BatchDownloadMessagesAsync ( IEnumerable < string > messageIds , CancellationToken cancellationToken = default )
{
var totalDownloadCount = messageIds . Count ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( totalDownloadCount = = 0 ) return ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var downloadedItemCount = 0 ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
_logger . Debug ( "Batch downloading {Count} messages for {Name}" , messageIds . Count ( ) , Account . Name ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var allDownloadRequests = messageIds . Select ( CreateSingleMessageGet ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Respect the batch size limit for batch requests.
var batchedDownloadRequests = allDownloadRequests . Batch ( ( int ) MaximumAllowedBatchRequestSize ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
_logger . Debug ( "Total items to download: {TotalDownloadCount}. Created {Count} batch download requests for {Name}." , batchedDownloadRequests . Count ( ) , Account . Name , totalDownloadCount ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// 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.
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
var batchProcessCallbacks = new List < Task < Message > > ( ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
foreach ( var batchBundle in batchedDownloadRequests )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
var batchRequest = new BatchRequest ( _gmailService ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Queue each request into this batch.
batchBundle . ForEach ( request = >
{
batchRequest . Queue < Message > ( request , ( content , error , index , message ) = >
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
var downloadingMessageId = messageIds . ElementAt ( index ) ;
2025-02-22 23:09:53 +01:00
var downloadTask = HandleSingleItemDownloadedCallbackAsync ( content , error , downloadingMessageId , cancellationToken ) ;
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
batchProcessCallbacks . Add ( downloadTask ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
downloadedItemCount + + ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var progressValue = downloadedItemCount * 100 / Math . Max ( 1 , totalDownloadCount ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
PublishSynchronizationProgress ( progressValue ) ;
2024-04-18 01:44:37 +02:00
} ) ;
2025-02-16 11:54:23 +01:00
} ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
_logger . Information ( "Executing batch download with {Count} items." , batchRequest . Count ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await batchRequest . ExecuteAsync ( cancellationToken ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// 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
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
GC . Collect ( ) ;
2025-02-16 11:43:30 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Wait for all processing to finish.
await Task . WhenAll ( batchProcessCallbacks ) . ConfigureAwait ( false ) ;
2025-02-22 23:09:53 +01:00
// Try to update max history id.
var maxHistoryId = batchProcessCallbacks . Select ( a = > a . Result ) . Where ( a = > a . HistoryId ! = null ) . Max ( a = > a . HistoryId . Value ) ;
if ( maxHistoryId ! = 0 )
{
Account . SynchronizationDeltaIdentifier = await _gmailChangeProcessor . UpdateAccountDeltaSynchronizationIdentifierAsync ( Account . Id , maxHistoryId . ToString ( ) ) . ConfigureAwait ( false ) ;
}
2025-02-16 11:54:23 +01:00
}
/// <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 ( ) ) ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
foreach ( var history in listHistoryResponse . History )
{
// Handle label additions.
if ( history . LabelsAdded is not null )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
foreach ( var addedLabel in history . LabelsAdded )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
await HandleLabelAssignmentAsync ( addedLabel ) ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Handle label removals.
if ( history . LabelsRemoved is not null )
{
foreach ( var removedLabel in history . LabelsRemoved )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
await HandleLabelRemovalAsync ( removedLabel ) ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Handle removed messages.
if ( history . MessagesDeleted is not null )
{
foreach ( var deletedMessage in history . MessagesDeleted )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
var messageId = deletedMessage . Message . Id ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
_logger . Debug ( "Processing message deletion for {MessageId}" , messageId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await _gmailChangeProcessor . DeleteMailAsync ( Account . Id , messageId ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
}
}
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-23 17:05:46 +01:00
private async Task HandleArchiveAssignmentAsync ( string archivedMessageId )
{
// Ignore if the message is already in the archive.
bool archived = await _gmailChangeProcessor . IsMailExistsInFolderAsync ( archivedMessageId , archiveFolderId . Value ) ;
if ( archived ) return ;
_logger . Debug ( "Processing archive assignment for message {Id}" , archivedMessageId ) ;
await _gmailChangeProcessor . CreateAssignmentAsync ( Account . Id , archivedMessageId , ServiceConstants . ARCHIVE_LABEL_ID ) . ConfigureAwait ( false ) ;
}
private async Task HandleUnarchiveAssignmentAsync ( string unarchivedMessageId )
{
// Ignore if the message is not in the archive.
bool archived = await _gmailChangeProcessor . IsMailExistsInFolderAsync ( unarchivedMessageId , archiveFolderId . Value ) ;
if ( ! archived ) return ;
_logger . Debug ( "Processing un-archive assignment for message {Id}" , unarchivedMessageId ) ;
await _gmailChangeProcessor . DeleteAssignmentAsync ( Account . Id , unarchivedMessageId , ServiceConstants . ARCHIVE_LABEL_ID ) . ConfigureAwait ( false ) ;
}
2025-02-16 11:54:23 +01:00
private async Task HandleLabelAssignmentAsync ( HistoryLabelAdded addedLabel )
{
var messageId = addedLabel . Message . Id ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
_logger . Debug ( "Processing label assignment for message {MessageId}" , messageId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
foreach ( var labelId in addedLabel . LabelIds )
{
// When UNREAD label is added mark the message as un-read.
if ( labelId = = ServiceConstants . UNREAD_LABEL_ID )
await _gmailChangeProcessor . ChangeMailReadStatusAsync ( messageId , false ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// When STARRED label is added mark the message as flagged.
if ( labelId = = ServiceConstants . STARRED_LABEL_ID )
await _gmailChangeProcessor . ChangeFlagStatusAsync ( messageId , true ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await _gmailChangeProcessor . CreateAssignmentAsync ( Account . Id , messageId , labelId ) . ConfigureAwait ( false ) ;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
private async Task HandleLabelRemovalAsync ( HistoryLabelRemoved removedLabel )
{
var messageId = removedLabel . Message . Id ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
_logger . Debug ( "Processing label removed for message {MessageId}" , messageId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
foreach ( var labelId in removedLabel . LabelIds )
{
// When UNREAD label is removed mark the message as read.
if ( labelId = = ServiceConstants . UNREAD_LABEL_ID )
await _gmailChangeProcessor . ChangeMailReadStatusAsync ( messageId , true ) . ConfigureAwait ( false ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
// When STARRED label is removed mark the message as un-flagged.
if ( labelId = = ServiceConstants . STARRED_LABEL_ID )
await _gmailChangeProcessor . ChangeFlagStatusAsync ( messageId , false ) . ConfigureAwait ( false ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
// For other labels remove the mail assignment.
await _gmailChangeProcessor . DeleteAssignmentAsync ( Account . Id , messageId , labelId ) . ConfigureAwait ( false ) ;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
/// <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 ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var nativeMessage = new Message ( )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
Raw = base64UrlEncodedMime ,
} ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( ! string . IsNullOrEmpty ( messageThreadId ) )
nativeMessage . ThreadId = messageThreadId ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var draft = new Draft ( )
{
Message = nativeMessage ,
Id = messageDraftId
} ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return draft ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
#region Mail Integrations
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public override List < IRequestBundle < IClientServiceRequest > > Move ( BatchMoveRequest request )
{
var toFolder = request [ 0 ] . ToFolder ;
var fromFolder = request [ 0 ] . FromFolder ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +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-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var batchModifyRequest = new BatchModifyMessagesRequest
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
Ids = request . Select ( a = > a . Item . Id . ToString ( ) ) . ToList ( ) ,
AddLabelIds = [ toFolder . RemoteFolderId ]
} ;
2024-11-26 20:03:10 +01:00
2025-02-23 17:05:46 +01:00
// Archived item is being moved to different folder.
// Unarchive will move it to Inbox, so this is a different case.
// We can't remove ARCHIVE label because it's a virtual folder and does not exist in Gmail.
// We will just add the target label and Gmail will handle the rest.
if ( fromFolder . SpecialFolderType = = SpecialFolderType . Archive )
2025-02-16 11:54:23 +01:00
{
2025-02-23 17:05:46 +01:00
batchModifyRequest . AddLabelIds = [ toFolder . RemoteFolderId ] ;
}
else if ( fromFolder . SpecialFolderType ! = SpecialFolderType . Sent )
{
// Only add remove label ids if the source folder is not sent folder.
2025-02-16 11:54:23 +01:00
batchModifyRequest . RemoveLabelIds = [ fromFolder . RemoteFolderId ] ;
}
2024-11-26 20:03:10 +01:00
2025-02-16 11:54:23 +01:00
var networkCall = _gmailService . Users . Messages . BatchModify ( batchModifyRequest , "me" ) ;
2024-11-26 20:03:10 +01:00
2025-02-16 11:54:23 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , request ) ] ;
}
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
public override List < IRequestBundle < IClientServiceRequest > > ChangeFlag ( BatchChangeFlagRequest request )
{
bool isFlagged = request [ 0 ] . IsFlagged ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var batchModifyRequest = new BatchModifyMessagesRequest
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
Ids = request . Select ( a = > a . Item . Id . ToString ( ) ) . ToList ( ) ,
} ;
2024-11-26 20:03:10 +01:00
2025-02-16 11:54:23 +01:00
if ( isFlagged )
batchModifyRequest . AddLabelIds = new List < string > ( ) { ServiceConstants . STARRED_LABEL_ID } ;
else
batchModifyRequest . RemoveLabelIds = new List < string > ( ) { ServiceConstants . STARRED_LABEL_ID } ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var networkCall = _gmailService . Users . Messages . BatchModify ( batchModifyRequest , "me" ) ;
2024-11-26 20:03:10 +01:00
2025-02-16 11:54:23 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , request ) ] ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public override List < IRequestBundle < IClientServiceRequest > > MarkRead ( BatchMarkReadRequest request )
{
bool readStatus = request [ 0 ] . IsRead ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var batchModifyRequest = new BatchModifyMessagesRequest
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
Ids = request . Select ( a = > a . Item . Id . ToString ( ) ) . ToList ( ) ,
} ;
2024-11-26 20:03:10 +01:00
2025-02-16 11:54:23 +01:00
if ( readStatus )
batchModifyRequest . RemoveLabelIds = new List < string > ( ) { ServiceConstants . UNREAD_LABEL_ID } ;
else
batchModifyRequest . AddLabelIds = new List < string > ( ) { ServiceConstants . UNREAD_LABEL_ID } ;
2024-11-26 20:03:10 +01:00
2025-02-16 11:54:23 +01:00
var networkCall = _gmailService . Users . Messages . BatchModify ( batchModifyRequest , "me" ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , request ) ] ;
}
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
public override List < IRequestBundle < IClientServiceRequest > > Delete ( BatchDeleteRequest request )
{
var batchModifyRequest = new BatchDeleteMessagesRequest
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
Ids = request . Select ( a = > a . Item . Id . ToString ( ) ) . ToList ( ) ,
} ;
2024-11-26 20:03:10 +01:00
2025-02-16 11:54:23 +01:00
var networkCall = _gmailService . Users . Messages . BatchDelete ( batchModifyRequest , "me" ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , request ) ] ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public override List < IRequestBundle < IClientServiceRequest > > CreateDraft ( CreateDraftRequest singleRequest )
{
Draft draft = null ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +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
2025-02-16 11:54:23 +01:00
var networkCall = _gmailService . Users . Drafts . Create ( draft , "me" ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , singleRequest , singleRequest ) ] ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public override List < IRequestBundle < IClientServiceRequest > > Archive ( BatchArchiveRequest request )
{
bool isArchiving = request [ 0 ] . IsArchiving ;
var batchModifyRequest = new BatchModifyMessagesRequest
2024-06-21 23:48:03 +02:00
{
2025-02-16 11:54:23 +01:00
Ids = request . Select ( a = > a . Item . Id . ToString ( ) ) . ToList ( )
} ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
if ( isArchiving )
{
batchModifyRequest . RemoveLabelIds = new [ ] { ServiceConstants . INBOX_LABEL_ID } ;
2024-06-21 23:48:03 +02:00
}
2025-02-16 11:54:23 +01:00
else
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
batchModifyRequest . AddLabelIds = new [ ] { ServiceConstants . INBOX_LABEL_ID } ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var networkCall = _gmailService . Users . Messages . BatchModify ( batchModifyRequest , "me" ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , request ) ] ;
}
2024-06-14 00:39:18 +02:00
2025-02-16 11:54:23 +01:00
public override List < IRequestBundle < IClientServiceRequest > > SendDraft ( SendDraftRequest singleDraftRequest )
{
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var message = new Message ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( ! string . IsNullOrEmpty ( singleDraftRequest . Item . ThreadId ) )
{
message . ThreadId = singleDraftRequest . Item . ThreadId ;
}
2024-11-26 20:03:10 +01:00
2025-02-16 11:54:23 +01:00
singleDraftRequest . Request . Mime . Prepare ( EncodingConstraint . None ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var mimeString = singleDraftRequest . Request . Mime . ToString ( ) ;
var base64UrlEncodedMime = Base64UrlEncoder . Encode ( mimeString ) ;
message . Raw = base64UrlEncodedMime ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var draft = new Draft ( )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
Id = singleDraftRequest . Request . MailItem . DraftId ,
Message = message
} ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:54:23 +01:00
var networkCall = _gmailService . Users . Drafts . Send ( draft , "me" ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:54:23 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , singleDraftRequest , singleDraftRequest ) ] ;
}
2024-07-09 01:05:16 +02:00
2025-02-22 00:22:00 +01:00
public override async Task < List < MailCopy > > OnlineSearchAsync ( string queryText , List < IMailItemFolder > folders , CancellationToken cancellationToken = default )
{
var request = _gmailService . Users . Messages . List ( "me" ) ;
request . Q = queryText ;
request . MaxResults = 500 ; // Max 500 is returned.
string pageToken = null ;
var messagesToDownload = new List < Message > ( ) ;
do
{
2025-02-23 10:21:58 +01:00
if ( queryText . StartsWith ( "label:" ) | | queryText . StartsWith ( "in:" ) )
{
// Ignore the folders if the query starts with these keywords.
// User is trying to list everything.
}
else if ( folders ? . Any ( ) ? ? false )
2025-02-22 00:22:00 +01:00
{
request . LabelIds = folders . Select ( a = > a . RemoteFolderId ) . ToList ( ) ;
}
if ( ! string . IsNullOrEmpty ( pageToken ) )
{
request . PageToken = pageToken ;
}
var response = await request . ExecuteAsync ( cancellationToken ) ;
if ( response . Messages = = null ) break ;
// Handle skipping manually
foreach ( var message in response . Messages )
{
messagesToDownload . Add ( message ) ;
}
pageToken = response . NextPageToken ;
} while ( ! string . IsNullOrEmpty ( pageToken ) ) ;
// Do not download messages that exists, but return them for listing.
var messageIds = messagesToDownload . Select ( a = > a . Id ) . ToList ( ) ;
List < string > downloadRequireMessageIds = new ( ) ;
foreach ( var messageId in messageIds )
{
var exists = await _gmailChangeProcessor . IsMailExistsAsync ( messageId ) . ConfigureAwait ( false ) ;
if ( ! exists )
{
downloadRequireMessageIds . Add ( messageId ) ;
}
}
// Download missing messages.
await BatchDownloadMessagesAsync ( downloadRequireMessageIds , cancellationToken ) ;
// Get results from database and return.
var searchResults = new List < MailCopy > ( ) ;
foreach ( var messageId in messageIds )
{
var copy = await _gmailChangeProcessor . GetMailCopyAsync ( messageId ) . ConfigureAwait ( false ) ;
if ( copy = = null ) continue ;
searchResults . Add ( copy ) ;
}
return searchResults ;
// TODO: Return the search result ids.
}
2025-02-16 11:54:23 +01:00
public override async Task DownloadMissingMimeMessageAsync ( IMailItem mailItem ,
ITransferProgress transferProgress = null ,
CancellationToken cancellationToken = default )
{
var request = _gmailService . Users . Messages . Get ( "me" , mailItem . Id ) ;
request . Format = UsersResource . MessagesResource . GetRequest . FormatEnum . Raw ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:54:23 +01:00
var gmailMessage = await request . ExecuteAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
var mimeMessage = gmailMessage . GetGmailMimeMessage ( ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:54:23 +01:00
if ( mimeMessage = = null )
{
_logger . Warning ( "Tried to download Gmail Raw Mime with {Id} id and server responded without a data." , mailItem . Id ) ;
return ;
2025-02-16 11:43:30 +01:00
}
2024-07-09 01:05:16 +02:00
2025-02-16 11:54:23 +01:00
await _gmailChangeProcessor . SaveMimeFileAsync ( mailItem . FileId , mimeMessage , Account . Id ) . ConfigureAwait ( false ) ;
}
public override List < IRequestBundle < IClientServiceRequest > > RenameFolder ( RenameFolderRequest request )
{
var label = new Label ( )
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
Name = request . NewFolderName
} ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var networkCall = _gmailService . Users . Labels . Update ( label , "me" , request . Folder . RemoteFolderId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return [ new HttpRequestBundle < IClientServiceRequest > ( networkCall , request , request ) ] ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public override List < IRequestBundle < IClientServiceRequest > > EmptyFolder ( EmptyFolderRequest request )
{
// Create batch delete request.
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
var deleteRequests = request . MailsToDelete . Select ( a = > new DeleteRequest ( a ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return Delete ( new BatchDeleteRequest ( deleteRequests ) ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public override List < IRequestBundle < IClientServiceRequest > > MarkFolderAsRead ( MarkFolderAsReadRequest request )
= > MarkRead ( new BatchMarkReadRequest ( request . MailsToMarkRead . Select ( a = > new MarkReadRequest ( a , true ) ) ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
#endregion
#region Request Execution
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public override async Task ExecuteNativeRequestsAsync ( List < IRequestBundle < IClientServiceRequest > > batchedRequests ,
CancellationToken cancellationToken = default )
{
var batchedBundles = batchedRequests . Batch ( ( int ) MaximumAllowedBatchRequestSize ) ;
var bundleCount = batchedBundles . Count ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
for ( int i = 0 ; i < bundleCount ; i + + )
{
var bundle = batchedBundles . ElementAt ( i ) ;
2024-08-29 22:43:27 +02:00
2025-02-16 11:54:23 +01:00
var nativeBatchRequest = new BatchRequest ( _gmailService ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
var bundleRequestCount = bundle . Count ( ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
var bundleTasks = new List < Task > ( ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
for ( int k = 0 ; k < bundleRequestCount ; k + + )
{
var requestBundle = bundle . ElementAt ( k ) ;
requestBundle . UIChangeRequest ? . ApplyUIChanges ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
nativeBatchRequest . Queue < object > ( requestBundle . NativeRequest , ( content , error , index , message )
= > bundleTasks . Add ( ProcessSingleNativeRequestResponseAsync ( requestBundle , error , message , cancellationToken ) ) ) ;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
await nativeBatchRequest . ExecuteAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
await Task . WhenAll ( bundleTasks ) ;
2025-02-16 11:35:43 +01:00
}
2025-02-16 11:54:23 +01:00
}
private void ProcessGmailRequestError ( RequestError error , IRequestBundle < IClientServiceRequest > bundle )
{
if ( error = = null ) return ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// OutOfMemoryException is a known bug in Gmail SDK.
if ( error . Code = = 0 )
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
bundle ? . UIChangeRequest ? . RevertUIChanges ( ) ;
throw new OutOfMemoryException ( error . Message ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Entity not found.
if ( error . Code = = 404 )
{
bundle ? . UIChangeRequest ? . RevertUIChanges ( ) ;
throw new SynchronizerEntityNotFoundException ( error . Message ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( ! string . IsNullOrEmpty ( error . Message ) )
{
bundle ? . UIChangeRequest ? . RevertUIChanges ( ) ;
error . Errors ? . ForEach ( error = > _logger . Error ( "Unknown Gmail SDK error for {Name}\n{Error}" , Account . Name , error ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
throw new SynchronizerException ( error . Message ) ;
}
}
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01: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>
2025-02-22 23:09:53 +01:00
private async Task < Message > HandleSingleItemDownloadedCallbackAsync ( Message message ,
2025-02-16 11:54:23 +01:00
RequestError error ,
string downloadingMessageId ,
CancellationToken cancellationToken = default )
{
try
{
ProcessGmailRequestError ( error , null ) ;
}
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 ) ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
if ( message = = null )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
_logger . Warning ( "Skipped GMail message download for {DownloadingMessageId}" , downloadingMessageId ) ;
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
return null ;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Gmail has LabelId property for each message.
// Therefore we can pass null as the assigned folder safely.
var mailPackage = await CreateNewMailPackagesAsync ( message , null , cancellationToken ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// If CreateNewMailPackagesAsync returns null it means local draft mapping is done.
// We don't need to insert anything else.
2025-02-22 23:09:53 +01:00
if ( mailPackage = = null ) return message ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
foreach ( var package in mailPackage )
{
await _gmailChangeProcessor . CreateMailAsync ( Account . Id , package ) . ConfigureAwait ( false ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
return message ;
}
private async Task UpdateAccountSyncIdentifierFromMessageAsync ( Message message )
{
2025-02-16 11:54:23 +01:00
// Try updating the history change identifier if any.
if ( message . HistoryId = = null ) return ;
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
if ( ulong . TryParse ( Account . SynchronizationDeltaIdentifier , out ulong currentIdentifier ) & &
ulong . TryParse ( message . HistoryId . Value . ToString ( ) , out ulong messageIdentifier ) & &
messageIdentifier > currentIdentifier )
{
Account . SynchronizationDeltaIdentifier = await _gmailChangeProcessor . UpdateAccountDeltaSynchronizationIdentifierAsync ( Account . Id , message . HistoryId . ToString ( ) ) ;
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
private async Task ProcessSingleNativeRequestResponseAsync ( IRequestBundle < IClientServiceRequest > bundle ,
RequestError error ,
HttpResponseMessage httpResponseMessage ,
CancellationToken cancellationToken = default )
{
ProcessGmailRequestError ( error , bundle ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( bundle is HttpRequestBundle < IClientServiceRequest , Message > messageBundle )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
var gmailMessage = await messageBundle . DeserializeBundleAsync ( httpResponseMessage , cancellationToken ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( gmailMessage = = null ) return ;
2024-04-18 01:44:37 +02:00
2025-02-22 23:09:53 +01:00
await HandleSingleItemDownloadedCallbackAsync ( gmailMessage , error , string . Empty , cancellationToken ) ;
await UpdateAccountSyncIdentifierFromMessageAsync ( gmailMessage ) . ConfigureAwait ( false ) ;
2025-02-16 11:54:23 +01:00
}
else if ( bundle is HttpRequestBundle < IClientServiceRequest , Label > folderBundle )
{
var gmailLabel = await folderBundle . DeserializeBundleAsync ( httpResponseMessage , cancellationToken ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( gmailLabel = = null ) return ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// 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.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var messageDraft = await draftBundle . DeserializeBundleAsync ( httpResponseMessage , cancellationToken ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( messageDraft = = null ) return ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var localDraftCopy = createDraftRequest . DraftPreperationRequest . CreatedLocalDraftCopy ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Here we have DraftId, MessageId and ThreadId.
// Update the local copy properties and re-synchronize to get the original message and update history.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// 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.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await _gmailChangeProcessor . MapLocalDraftAsync ( Account . Id , localDraftCopy . UniqueId , messageDraft . Message . Id , messageDraft . Id , messageDraft . Message . ThreadId ) ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
var options = new MailSynchronizationOptions ( )
{
AccountId = Account . Id ,
Type = MailSynchronizationType . FullFolders
} ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await SynchronizeMailsInternalAsync ( options , cancellationToken ) ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-23 17:05:46 +01:00
/// <summary>
/// Gmail Archive is a special folder that is not visible in the Gmail web interface.
/// We need to handle it separately.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
private async Task MapArchivedMailsAsync ( CancellationToken cancellationToken )
{
var request = _gmailService . Users . Messages . List ( "me" ) ;
request . Q = "in:archive" ;
request . MaxResults = InitialMessageDownloadCountPerFolder ;
string pageToken = null ;
var archivedMessageIds = new List < string > ( ) ;
do
{
if ( ! string . IsNullOrEmpty ( pageToken ) ) request . PageToken = pageToken ;
var response = await request . ExecuteAsync ( cancellationToken ) ;
if ( response . Messages = = null ) break ;
foreach ( var message in response . Messages )
{
if ( archivedMessageIds . Contains ( message . Id ) ) continue ;
archivedMessageIds . Add ( message . Id ) ;
}
pageToken = response . NextPageToken ;
} while ( ! string . IsNullOrEmpty ( pageToken ) ) ;
var result = await _gmailChangeProcessor . GetGmailArchiveComparisonResultAsync ( archiveFolderId . Value , archivedMessageIds ) . ConfigureAwait ( false ) ;
foreach ( var archiveAddedItem in result . Added )
{
await HandleArchiveAssignmentAsync ( archiveAddedItem ) ;
}
foreach ( var unAarchivedRemovedItem in result . Removed )
{
await HandleUnarchiveAssignmentAsync ( unAarchivedRemovedItem ) ;
}
}
2025-02-16 11:54:23 +01: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 )
{
2025-02-22 23:09:53 +01:00
// Check if account has any draft locally.
// There is no point to send this query if there are no local drafts.
bool hasLocalDrafts = await _gmailChangeProcessor . HasAccountAnyDraftAsync ( Account . Id ) . ConfigureAwait ( false ) ;
if ( ! hasLocalDrafts ) return ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var drafts = await _gmailService . Users . Drafts . List ( "me" ) . ExecuteAsync ( cancellationToken ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( drafts . Drafts = = null )
{
_logger . Information ( "There are no drafts to map for {Name}" , Account . Name ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return ;
2025-02-16 11:35:43 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
foreach ( var draft in drafts . Drafts )
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
await _gmailChangeProcessor . MapLocalDraftAsync ( draft . Message . Id , draft . Id , draft . Message . ThreadId ) ;
}
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
/// <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 > ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
MimeMessage mimeMessage = message . GetGmailMimeMessage ( ) ;
var mailCopy = message . AsMailCopy ( mimeMessage ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// 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-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( mailCopy . IsDraft
& & mimeMessage . Headers . Contains ( Domain . Constants . WinoLocalDraftHeader )
& & 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.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
bool isMappingSuccesfull = await _gmailChangeProcessor . MapLocalDraftAsync ( Account . Id , localDraftCopyUniqueId , mailCopy . Id , mailCopy . DraftId , mailCopy . ThreadId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( isMappingSuccesfull ) return null ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
// Local copy doesn't exists. Continue execution to insert mail copy.
}
if ( message . LabelIds is not null )
{
foreach ( var labelId in message . LabelIds )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
packageList . Add ( new NewMailItemPackage ( mailCopy , mimeMessage , labelId ) ) ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:43:30 +01:00
}
2025-02-15 12:53:32 +01:00
2025-02-16 11:54:23 +01:00
return packageList ;
}
2025-02-15 12:53:32 +01:00
2025-02-16 11:54:23 +01:00
#endregion
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +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
}
}