2024-04-18 01:44:37 +02:00
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.IO ;
using System.Linq ;
using System.Net ;
2024-08-19 19:02:33 +02:00
using System.Text ;
2024-08-21 13:15:50 +02:00
using System.Text.Json ;
2024-08-19 19:16:47 +02:00
using System.Text.Json.Nodes ;
2025-02-14 01:43:52 +01:00
using System.Text.Json.Serialization ;
2024-04-18 01:44:37 +02:00
using System.Text.RegularExpressions ;
using System.Threading ;
using System.Threading.Tasks ;
using Microsoft.Graph ;
using Microsoft.Graph.Models ;
2024-08-26 22:09:00 +02:00
using Microsoft.Graph.Models.ODataErrors ;
2024-04-18 01:44:37 +02:00
using Microsoft.Kiota.Abstractions ;
using Microsoft.Kiota.Abstractions.Authentication ;
2024-08-05 00:36:26 +02:00
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware ;
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options ;
2024-04-18 01:44:37 +02:00
using MimeKit ;
using MoreLinq.Extensions ;
using Serilog ;
2025-01-06 02:15:21 +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-09-05 17:23:15 +02:00
using Wino.Core.Misc ;
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-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
{
2025-02-14 01:43:52 +01:00
[JsonSerializable(typeof(Microsoft.Graph.Me.Messages.Item.Move.MovePostRequestBody))]
[JsonSerializable(typeof(OutlookFileAttachment))]
public partial class OutlookSynchronizerJsonContext : JsonSerializerContext ;
2024-11-30 12:47:24 +01:00
public class OutlookSynchronizer : WinoSynchronizer < RequestInformation , Message , Event >
2024-04-18 01:44:37 +02:00
{
public override uint BatchModificationSize = > 20 ;
public override uint InitialMessageDownloadCountPerFolder = > 250 ;
private const uint MaximumAllowedBatchRequestSize = 20 ;
private const string INBOX_NAME = "inbox" ;
private const string SENT_NAME = "sentitems" ;
private const string DELETED_NAME = "deleteditems" ;
private const string JUNK_NAME = "junkemail" ;
private const string DRAFTS_NAME = "drafts" ;
private const string ARCHIVE_NAME = "archive" ;
private readonly string [ ] outlookMessageSelectParameters =
[
"InferenceClassification" ,
"Flag" ,
"Importance" ,
"IsRead" ,
"IsDraft" ,
"ReceivedDateTime" ,
"HasAttachments" ,
"BodyPreview" ,
"Id" ,
"ConversationId" ,
"From" ,
"Subject" ,
"ParentFolderId" ,
"InternetMessageId" ,
] ;
2024-07-09 01:05:16 +02:00
private readonly SemaphoreSlim _handleItemRetrievalSemaphore = new ( 1 ) ;
2024-04-18 01:44:37 +02:00
private readonly ILogger _logger = Log . ForContext < OutlookSynchronizer > ( ) ;
2024-05-25 17:00:52 +02:00
private readonly IOutlookChangeProcessor _outlookChangeProcessor ;
2024-04-18 01:44:37 +02:00
private readonly GraphServiceClient _graphClient ;
public OutlookSynchronizer ( MailAccount account ,
IAuthenticator authenticator ,
2024-05-25 17:00:52 +02:00
IOutlookChangeProcessor outlookChangeProcessor ) : base ( account )
2024-04-18 01:44:37 +02:00
{
var tokenProvider = new MicrosoftTokenProvider ( Account , authenticator ) ;
2024-08-05 00:36:26 +02:00
// Update request handlers for Graph client.
2024-04-18 01:44:37 +02:00
var handlers = GraphClientFactory . CreateDefaultHandlers ( ) ;
2024-08-05 00:36:26 +02:00
handlers . Add ( GetMicrosoftImmutableIdHandler ( ) ) ;
// Remove existing RetryHandler and add a new one with custom options.
var existingRetryHandler = handlers . FirstOrDefault ( a = > a is RetryHandler ) ;
if ( existingRetryHandler ! = null )
handlers . Remove ( existingRetryHandler ) ;
2024-04-18 01:44:37 +02:00
2024-08-05 00:36:26 +02:00
// Add custom one.
handlers . Add ( GetRetryHandler ( ) ) ;
var httpClient = GraphClientFactory . Create ( handlers ) ;
2024-04-18 01:44:37 +02:00
_graphClient = new GraphServiceClient ( httpClient , new BaseBearerTokenAuthenticationProvider ( tokenProvider ) ) ;
2024-08-05 00:36:26 +02:00
2024-04-18 01:44:37 +02:00
_outlookChangeProcessor = outlookChangeProcessor ;
}
2024-08-05 00:36:26 +02:00
#region MS Graph Handlers
private MicrosoftImmutableIdHandler GetMicrosoftImmutableIdHandler ( ) = > new ( ) ;
private RetryHandler GetRetryHandler ( )
{
var options = new RetryHandlerOption ( )
{
ShouldRetry = ( delay , attempt , httpResponse ) = >
{
var statusCode = httpResponse . StatusCode ;
return statusCode switch
{
HttpStatusCode . ServiceUnavailable = > true ,
HttpStatusCode . GatewayTimeout = > true ,
( HttpStatusCode ) 429 = > true ,
HttpStatusCode . Unauthorized = > true ,
_ = > false
} ;
} ,
Delay = 3 ,
MaxRetry = 3
} ;
return new RetryHandler ( options ) ;
}
#endregion
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
{
var downloadedMessageIds = new List < string > ( ) ;
_logger . Information ( "Internal synchronization started for {Name}" , Account . Name ) ;
_logger . Information ( "Options: {Options}" , options ) ;
try
{
2024-08-05 00:36:26 +02:00
PublishSynchronizationProgress ( 1 ) ;
2024-04-18 01:44:37 +02:00
await SynchronizeFoldersAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2024-12-24 18:30:25 +01:00
if ( options . Type ! = MailSynchronizationType . FoldersOnly )
2024-04-18 01:44:37 +02:00
{
var synchronizationFolders = await _outlookChangeProcessor . GetSynchronizationFoldersAsync ( options ) . ConfigureAwait ( false ) ;
2024-08-21 13:15:50 +02:00
_logger . Information ( string . Format ( "{1} Folders: {0}" , string . Join ( "," , synchronizationFolders . Select ( a = > a . FolderName ) ) , synchronizationFolders . Count ) ) ;
2024-06-02 21:35:03 +02:00
2024-04-18 01:44:37 +02:00
for ( int i = 0 ; i < synchronizationFolders . Count ; i + + )
{
var folder = synchronizationFolders [ i ] ;
var progress = ( int ) Math . Round ( ( double ) ( i + 1 ) / synchronizationFolders . Count * 100 ) ;
2024-08-05 00:36:26 +02:00
PublishSynchronizationProgress ( progress ) ;
2024-04-18 01:44:37 +02:00
var folderDownloadedMessageIds = await SynchronizeFolderAsync ( folder , cancellationToken ) . ConfigureAwait ( false ) ;
downloadedMessageIds . AddRange ( folderDownloadedMessageIds ) ;
}
}
}
catch ( Exception ex )
{
2024-08-05 00:36:26 +02:00
_logger . Error ( ex , "Synchronizing folders for {Name}" , Account . Name ) ;
Debugger . Break ( ) ;
2024-04-18 01:44:37 +02:00
throw ;
}
finally
{
2024-08-05 00:36:26 +02:00
PublishSynchronizationProgress ( 100 ) ;
2024-04-18 01:44:37 +02:00
}
// Get all unred new downloaded items and return in the result.
// This is primarily used in notifications.
var unreadNewItems = await _outlookChangeProcessor . GetDownloadedUnreadMailsAsync ( Account . Id , downloadedMessageIds ) . ConfigureAwait ( false ) ;
2024-12-24 18:30:25 +01:00
return MailSynchronizationResult . Completed ( unreadNewItems ) ;
2024-04-18 01:44:37 +02:00
}
private async Task < IEnumerable < string > > SynchronizeFolderAsync ( MailItemFolder folder , CancellationToken cancellationToken = default )
{
var downloadedMessageIds = new List < string > ( ) ;
cancellationToken . ThrowIfCancellationRequested ( ) ;
string latestDeltaLink = string . Empty ;
bool isInitialSync = string . IsNullOrEmpty ( folder . DeltaToken ) ;
Microsoft . Graph . Me . MailFolders . Item . Messages . Delta . DeltaGetResponse messageCollectionPage = null ;
2024-08-21 13:15:50 +02:00
_logger . Debug ( "Synchronizing {FolderName}" , folder . FolderName ) ;
2024-04-18 01:44:37 +02:00
if ( isInitialSync )
{
2024-06-02 21:35:03 +02:00
_logger . Debug ( "No sync identifier for Folder {FolderName}. Performing initial sync." , folder . FolderName ) ;
2024-04-18 01:44:37 +02:00
// No delta link. Performing initial sync.
messageCollectionPage = await _graphClient . Me . MailFolders [ folder . RemoteFolderId ] . Messages . Delta . GetAsDeltaGetResponseAsync ( ( config ) = >
{
config . QueryParameters . Top = ( int ) InitialMessageDownloadCountPerFolder ;
config . QueryParameters . Select = outlookMessageSelectParameters ;
config . QueryParameters . Orderby = [ "receivedDateTime desc" ] ;
} , cancellationToken ) . ConfigureAwait ( false ) ;
}
else
{
var currentDeltaToken = folder . DeltaToken ;
var requestInformation = _graphClient . Me . MailFolders [ folder . RemoteFolderId ] . Messages . Delta . ToGetRequestInformation ( ( config ) = >
{
config . QueryParameters . Top = ( int ) InitialMessageDownloadCountPerFolder ;
config . QueryParameters . Select = outlookMessageSelectParameters ;
config . QueryParameters . Orderby = [ "receivedDateTime desc" ] ;
} ) ;
requestInformation . UrlTemplate = requestInformation . UrlTemplate . Insert ( requestInformation . UrlTemplate . Length - 1 , ",%24deltatoken" ) ;
requestInformation . QueryParameters . Add ( "%24deltatoken" , currentDeltaToken ) ;
messageCollectionPage = await _graphClient . RequestAdapter . SendAsync ( requestInformation , Microsoft . Graph . Me . MailFolders . Item . Messages . Delta . DeltaGetResponse . CreateFromDiscriminatorValue ) ;
}
var messageIteratorAsync = PageIterator < Message , Microsoft . Graph . Me . MailFolders . Item . Messages . Delta . DeltaGetResponse > . CreatePageIterator ( _graphClient , messageCollectionPage , async ( item ) = >
{
2024-07-09 01:05:16 +02:00
try
{
await _handleItemRetrievalSemaphore . WaitAsync ( ) ;
return await HandleItemRetrievedAsync ( item , folder , downloadedMessageIds , cancellationToken ) ;
}
catch ( Exception ex )
{
_logger . Error ( ex , "Error occurred while handling item {Id} for folder {FolderName}" , item . Id , folder . FolderName ) ;
}
finally
{
_handleItemRetrievalSemaphore . Release ( ) ;
}
return true ;
2024-04-18 01:44:37 +02:00
} ) ;
await messageIteratorAsync
. IterateAsync ( cancellationToken )
. ConfigureAwait ( false ) ;
latestDeltaLink = messageIteratorAsync . Deltalink ;
2024-06-02 21:35:03 +02:00
if ( downloadedMessageIds . Any ( ) )
{
_logger . Debug ( "Downloaded {Count} messages for folder {FolderName}" , downloadedMessageIds . Count , folder . FolderName ) ;
}
2024-04-18 01:44:37 +02:00
//Store delta link for tracking new changes.
if ( ! string . IsNullOrEmpty ( latestDeltaLink ) )
{
// Parse Delta Token from Delta Link since v5 of Graph SDK works based on the token, not the link.
2024-05-25 17:00:52 +02:00
var deltaToken = GetDeltaTokenFromDeltaLink ( latestDeltaLink ) ;
2024-04-18 01:44:37 +02:00
await _outlookChangeProcessor . UpdateFolderDeltaSynchronizationIdentifierAsync ( folder . Id , deltaToken ) . ConfigureAwait ( false ) ;
}
2024-06-02 21:35:03 +02:00
await _outlookChangeProcessor . UpdateFolderLastSyncDateAsync ( folder . Id ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
return downloadedMessageIds ;
}
2024-05-25 17:00:52 +02:00
private string GetDeltaTokenFromDeltaLink ( string deltaLink )
= > Regex . Split ( deltaLink , "deltatoken=" ) [ 1 ] ;
2024-04-18 01:44:37 +02:00
private bool IsResourceDeleted ( IDictionary < string , object > additionalData )
= > additionalData ! = null & & additionalData . ContainsKey ( "@removed" ) ;
private async Task < bool > HandleFolderRetrievedAsync ( MailFolder folder , OutlookSpecialFolderIdInformation outlookSpecialFolderIdInformation , CancellationToken cancellationToken = default )
{
if ( IsResourceDeleted ( folder . AdditionalData ) )
{
await _outlookChangeProcessor . DeleteFolderAsync ( Account . Id , folder . Id ) . ConfigureAwait ( false ) ;
}
else
{
// New folder created.
var item = folder . GetLocalFolder ( Account . Id ) ;
if ( item . RemoteFolderId . Equals ( outlookSpecialFolderIdInformation . InboxId ) )
item . SpecialFolderType = SpecialFolderType . Inbox ;
else if ( item . RemoteFolderId . Equals ( outlookSpecialFolderIdInformation . SentId ) )
item . SpecialFolderType = SpecialFolderType . Sent ;
else if ( item . RemoteFolderId . Equals ( outlookSpecialFolderIdInformation . DraftId ) )
item . SpecialFolderType = SpecialFolderType . Draft ;
else if ( item . RemoteFolderId . Equals ( outlookSpecialFolderIdInformation . TrashId ) )
item . SpecialFolderType = SpecialFolderType . Deleted ;
else if ( item . RemoteFolderId . Equals ( outlookSpecialFolderIdInformation . JunkId ) )
item . SpecialFolderType = SpecialFolderType . Junk ;
else if ( item . RemoteFolderId . Equals ( outlookSpecialFolderIdInformation . ArchiveId ) )
item . SpecialFolderType = SpecialFolderType . Archive ;
else
item . SpecialFolderType = SpecialFolderType . Other ;
// Automatically mark special folders as Sticky for better visibility.
item . IsSticky = item . SpecialFolderType ! = SpecialFolderType . Other ;
// By default, all non-others are system folder.
item . IsSystemFolder = item . SpecialFolderType ! = SpecialFolderType . Other ;
// By default, all special folders update unread count in the UI except Trash.
item . ShowUnreadCount = item . SpecialFolderType ! = SpecialFolderType . Deleted | | item . SpecialFolderType ! = SpecialFolderType . Other ;
await _outlookChangeProcessor . InsertFolderAsync ( item ) . ConfigureAwait ( false ) ;
}
return true ;
}
2024-09-13 02:51:37 +02:00
/// <summary>
/// Somehow, Graph API returns Message type item for items like TodoTask, EventMessage and Contact.
/// Basically deleted item retention items are stored as Message object in Deleted Items folder.
/// Suprisingly, odatatype will also be the same as Message.
/// In order to differentiate them from regular messages, we need to check the addresses in the message.
/// </summary>
/// <param name="item">Retrieved message.</param>
/// <returns>Whether the item is non-Message type or not.</returns>
private bool IsNotRealMessageType ( Message item )
= > item is EventMessage | | item . From ? . EmailAddress = = null ;
2024-04-18 01:44:37 +02:00
private async Task < bool > HandleItemRetrievedAsync ( Message item , MailItemFolder folder , IList < string > downloadedMessageIds , CancellationToken cancellationToken = default )
{
if ( IsResourceDeleted ( item . AdditionalData ) )
{
// Deleting item with this override instead of the other one that deletes all mail copies.
// Outlook mails have 1 assignment per-folder, unlike Gmail that has one to many.
await _outlookChangeProcessor . DeleteAssignmentAsync ( Account . Id , item . Id , folder . RemoteFolderId ) . ConfigureAwait ( false ) ;
}
2024-08-05 00:36:26 +02:00
else
2024-04-18 01:44:37 +02:00
{
2024-08-05 00:36:26 +02:00
// If the item exists in the local database, it means that it's already downloaded. Process as an Update.
2024-04-18 01:44:37 +02:00
2024-08-05 00:36:26 +02:00
var isMailExists = await _outlookChangeProcessor . IsMailExistsInFolderAsync ( item . Id , folder . Id ) ;
2024-04-18 01:44:37 +02:00
2024-08-05 00:36:26 +02:00
if ( isMailExists )
2024-04-18 01:44:37 +02:00
{
2024-08-05 00:36:26 +02:00
// Some of the properties of the item are updated.
2024-04-18 01:44:37 +02:00
2024-08-05 00:36:26 +02:00
if ( item . IsRead ! = null )
{
await _outlookChangeProcessor . ChangeMailReadStatusAsync ( item . Id , item . IsRead . GetValueOrDefault ( ) ) . ConfigureAwait ( false ) ;
}
2024-04-18 01:44:37 +02:00
2024-08-05 00:36:26 +02:00
if ( item . Flag ? . FlagStatus ! = null )
2024-04-18 01:44:37 +02:00
{
2024-08-05 00:36:26 +02:00
await _outlookChangeProcessor . ChangeFlagStatusAsync ( item . Id , item . Flag . FlagStatus . GetValueOrDefault ( ) = = FollowupFlagStatus . Flagged )
. ConfigureAwait ( false ) ;
}
}
else
{
2024-09-14 22:23:12 +02:00
if ( IsNotRealMessageType ( item ) )
{
if ( item is EventMessage eventMessage )
{
Log . Warning ( "Recieved event message. This is not supported yet. {Id}" , eventMessage . Id ) ;
}
else
{
Log . Warning ( "Recieved either contact or todo item as message This is not supported yet. {Id}" , item . Id ) ;
}
return true ;
}
2024-08-05 00:36:26 +02:00
// Package may return null on some cases mapping the remote draft to existing local draft.
var newMailPackages = await CreateNewMailPackagesAsync ( item , folder , cancellationToken ) ;
2024-04-18 01:44:37 +02:00
2024-08-05 00:36:26 +02:00
if ( newMailPackages ! = null )
{
foreach ( var package in newMailPackages )
2024-04-18 01:44:37 +02:00
{
2024-08-05 00:36:26 +02:00
// Only add to downloaded message ids if it's inserted successfuly.
// Updates should not be added to the list because they are not new.
bool isInserted = await _outlookChangeProcessor . CreateMailAsync ( Account . Id , package ) . ConfigureAwait ( false ) ;
if ( isInserted )
{
downloadedMessageIds . Add ( package . Copy . Id ) ;
}
2024-04-18 01:44:37 +02:00
}
}
}
}
return true ;
}
private async Task SynchronizeFoldersAsync ( CancellationToken cancellationToken = default )
{
2024-08-05 00:36:26 +02:00
// Gather special folders by default.
// Others will be other type.
2024-04-18 01:44:37 +02:00
2024-08-05 00:36:26 +02:00
// Get well known folder ids by batch.
2024-04-18 01:44:37 +02:00
2024-08-05 00:36:26 +02:00
retry :
2024-04-18 01:44:37 +02:00
var wellKnownFolderIdBatch = new BatchRequestContentCollection ( _graphClient ) ;
var inboxRequest = _graphClient . Me . MailFolders [ INBOX_NAME ] . ToGetRequestInformation ( ( t ) = > { t . QueryParameters . Select = [ "id" ] ; } ) ;
var sentRequest = _graphClient . Me . MailFolders [ SENT_NAME ] . ToGetRequestInformation ( ( t ) = > { t . QueryParameters . Select = [ "id" ] ; } ) ;
var deletedRequest = _graphClient . Me . MailFolders [ DELETED_NAME ] . ToGetRequestInformation ( ( t ) = > { t . QueryParameters . Select = [ "id" ] ; } ) ;
var junkRequest = _graphClient . Me . MailFolders [ JUNK_NAME ] . ToGetRequestInformation ( ( t ) = > { t . QueryParameters . Select = [ "id" ] ; } ) ;
var draftsRequest = _graphClient . Me . MailFolders [ DRAFTS_NAME ] . ToGetRequestInformation ( ( t ) = > { t . QueryParameters . Select = [ "id" ] ; } ) ;
var archiveRequest = _graphClient . Me . MailFolders [ ARCHIVE_NAME ] . ToGetRequestInformation ( ( t ) = > { t . QueryParameters . Select = [ "id" ] ; } ) ;
var inboxId = await wellKnownFolderIdBatch . AddBatchRequestStepAsync ( inboxRequest ) ;
var sentId = await wellKnownFolderIdBatch . AddBatchRequestStepAsync ( sentRequest ) ;
var deletedId = await wellKnownFolderIdBatch . AddBatchRequestStepAsync ( deletedRequest ) ;
var junkId = await wellKnownFolderIdBatch . AddBatchRequestStepAsync ( junkRequest ) ;
var draftsId = await wellKnownFolderIdBatch . AddBatchRequestStepAsync ( draftsRequest ) ;
var archiveId = await wellKnownFolderIdBatch . AddBatchRequestStepAsync ( archiveRequest ) ;
var returnedResponse = await _graphClient . Batch . PostAsync ( wellKnownFolderIdBatch , cancellationToken ) . ConfigureAwait ( false ) ;
var inboxFolderId = ( await returnedResponse . GetResponseByIdAsync < MailFolder > ( inboxId ) ) . Id ;
var sentFolderId = ( await returnedResponse . GetResponseByIdAsync < MailFolder > ( sentId ) ) . Id ;
var deletedFolderId = ( await returnedResponse . GetResponseByIdAsync < MailFolder > ( deletedId ) ) . Id ;
var junkFolderId = ( await returnedResponse . GetResponseByIdAsync < MailFolder > ( junkId ) ) . Id ;
var draftsFolderId = ( await returnedResponse . GetResponseByIdAsync < MailFolder > ( draftsId ) ) . Id ;
var archiveFolderId = ( await returnedResponse . GetResponseByIdAsync < MailFolder > ( archiveId ) ) . Id ;
var specialFolderInfo = new OutlookSpecialFolderIdInformation ( inboxFolderId , deletedFolderId , junkFolderId , draftsFolderId , sentFolderId , archiveFolderId ) ;
Microsoft . Graph . Me . MailFolders . Delta . DeltaGetResponse graphFolders = null ;
if ( string . IsNullOrEmpty ( Account . SynchronizationDeltaIdentifier ) )
{
// Initial folder sync.
var deltaRequest = _graphClient . Me . MailFolders . Delta . ToGetRequestInformation ( ) ;
deltaRequest . UrlTemplate = deltaRequest . UrlTemplate . Insert ( deltaRequest . UrlTemplate . Length - 1 , ",includehiddenfolders" ) ;
deltaRequest . QueryParameters . Add ( "includehiddenfolders" , "true" ) ;
graphFolders = await _graphClient . RequestAdapter . SendAsync ( deltaRequest ,
Microsoft . Graph . Me . MailFolders . Delta . DeltaGetResponse . CreateFromDiscriminatorValue ,
2024-07-09 01:05:16 +02:00
cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
}
else
{
var currentDeltaLink = Account . SynchronizationDeltaIdentifier ;
var deltaRequest = _graphClient . Me . MailFolders . Delta . ToGetRequestInformation ( ) ;
deltaRequest . UrlTemplate = deltaRequest . UrlTemplate . Insert ( deltaRequest . UrlTemplate . Length - 1 , ",%24deltaToken" ) ;
deltaRequest . QueryParameters . Add ( "%24deltaToken" , currentDeltaLink ) ;
2024-08-05 00:36:26 +02:00
try
{
graphFolders = await _graphClient . RequestAdapter . SendAsync ( deltaRequest ,
2024-04-18 01:44:37 +02:00
Microsoft . Graph . Me . MailFolders . Delta . DeltaGetResponse . CreateFromDiscriminatorValue ,
2024-07-09 01:05:16 +02:00
cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
2024-08-05 00:36:26 +02:00
}
catch ( ApiException apiException ) when ( apiException . ResponseStatusCode = = 410 )
{
Account . SynchronizationDeltaIdentifier = await _outlookChangeProcessor . ResetAccountDeltaTokenAsync ( Account . Id ) ;
goto retry ;
}
2024-04-18 01:44:37 +02:00
}
var iterator = PageIterator < MailFolder , Microsoft . Graph . Me . MailFolders . Delta . DeltaGetResponse > . CreatePageIterator ( _graphClient , graphFolders , ( folder ) = >
{
return HandleFolderRetrievedAsync ( folder , specialFolderInfo , cancellationToken ) ;
} ) ;
await iterator . IterateAsync ( ) ;
if ( ! string . IsNullOrEmpty ( iterator . Deltalink ) )
{
// Get the second part of the query that its the deltaToken
var deltaToken = iterator . Deltalink . Split ( '=' ) [ 1 ] ;
var latestAccountDeltaToken = await _outlookChangeProcessor . UpdateAccountDeltaSynchronizationIdentifierAsync ( Account . Id , deltaToken ) ;
if ( ! string . IsNullOrEmpty ( latestAccountDeltaToken ) )
{
Account . SynchronizationDeltaIdentifier = latestAccountDeltaToken ;
}
}
}
2024-08-16 00:37:38 +02:00
/// <summary>
/// Get the user's profile picture
/// </summary>
/// <returns>Base64 encoded profile picture.</returns>
private async Task < string > GetUserProfilePictureAsync ( )
{
2024-08-26 22:09:00 +02:00
try
{
var photoStream = await _graphClient . Me . Photos [ "48x48" ] . Content . GetAsync ( ) ;
using var memoryStream = new MemoryStream ( ) ;
await photoStream . CopyToAsync ( memoryStream ) ;
var byteArray = memoryStream . ToArray ( ) ;
2024-08-16 00:37:38 +02:00
2024-08-26 22:09:00 +02:00
return Convert . ToBase64String ( byteArray ) ;
}
catch ( ODataError odataError ) when ( odataError . Error . Code = = "ImageNotFound" )
{
// Accounts without profile picture will throw this error.
// At this point nothing we can do. Just return empty string.
2024-08-16 00:37:38 +02:00
2024-08-26 22:09:00 +02:00
return string . Empty ;
}
catch ( Exception )
{
2024-09-12 00:50:49 +02:00
// Don't throw for profile picture.
// Office 365 apps require different permissions for profile picture.
// This permission requires admin consent.
// We avoid those permissions for now.
return string . Empty ;
2024-08-26 22:09:00 +02:00
}
2024-08-16 00:37:38 +02:00
}
2024-08-19 20:43:26 +02:00
/// <summary>
/// Get the user's display name.
/// </summary>
2024-11-20 01:45:48 +01:00
/// <returns>Display name and address of the user.</returns>
private async Task < Tuple < string , string > > GetDisplayNameAndAddressAsync ( )
2024-08-16 00:37:38 +02:00
{
2024-09-29 01:45:59 +03:00
var userInfo = await _graphClient . Me . GetAsync ( ) ;
2024-08-16 00:37:38 +02:00
2024-11-20 01:45:48 +01:00
return new Tuple < string , string > ( userInfo . DisplayName , userInfo . Mail ) ;
2024-08-16 00:37:38 +02:00
}
2024-08-17 19:54:52 +02:00
public override async Task < ProfileInformation > GetProfileInformationAsync ( )
2024-08-16 00:37:38 +02:00
{
2024-08-17 03:43:37 +02:00
var profilePictureData = await GetUserProfilePictureAsync ( ) . ConfigureAwait ( false ) ;
2024-11-20 01:45:48 +01:00
var displayNameAndAddress = await GetDisplayNameAndAddressAsync ( ) . ConfigureAwait ( false ) ;
2024-08-16 00:37:38 +02:00
2024-11-20 01:45:48 +01:00
return new ProfileInformation ( displayNameAndAddress . Item1 , profilePictureData , displayNameAndAddress . Item2 ) ;
2024-08-16 00:37:38 +02:00
}
2024-08-21 13:15:50 +02:00
/// <summary>
/// POST requests are handled differently in batches in Graph SDK.
/// Batch basically ignores the step's coontent-type and body.
/// Manually create a POST request with empty body and send it.
/// </summary>
/// <param name="requestInformation">Post request information.</param>
/// <param name="content">Content object to serialize.</param>
/// <returns>Updated post request information.</returns>
2025-02-14 01:43:52 +01:00
private RequestInformation PreparePostRequestInformation ( RequestInformation requestInformation , Microsoft . Graph . Me . Messages . Item . Move . MovePostRequestBody content = null )
2024-08-21 13:15:50 +02:00
{
requestInformation . Headers . Clear ( ) ;
2025-02-14 01:43:52 +01:00
string contentJson = content = = null ? "{}" : JsonSerializer . Serialize ( content , OutlookSynchronizerJsonContext . Default . MovePostRequestBody ) ;
2024-08-21 13:15:50 +02:00
requestInformation . Content = new MemoryStream ( Encoding . UTF8 . GetBytes ( contentJson ) ) ;
requestInformation . HttpMethod = Method . POST ;
requestInformation . Headers . Add ( "Content-Type" , "application/json" ) ;
return requestInformation ;
}
2024-04-18 01:44:37 +02:00
#region Mail Integration
2024-06-12 02:12:39 +02:00
public override bool DelaySendOperationSynchronization ( ) = > true ;
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < RequestInformation > > Move ( BatchMoveRequest request )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
return ForEachRequest ( request , ( item ) = >
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
var requestBody = new Microsoft . Graph . Me . Messages . Item . Move . MovePostRequestBody ( )
{
DestinationId = item . ToFolder . RemoteFolderId
} ;
2024-04-18 01:44:37 +02:00
2025-02-14 01:43:52 +01:00
return PreparePostRequestInformation ( _graphClient . Me . Messages [ item . Item . Id ] . Move . ToPostRequestInformation ( requestBody ) ,
2024-11-26 20:03:10 +01:00
requestBody ) ;
2024-04-18 01:44:37 +02:00
} ) ;
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < RequestInformation > > ChangeFlag ( BatchChangeFlagRequest request )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
return ForEachRequest ( request , ( item ) = >
2024-04-18 01:44:37 +02:00
{
var message = new Message ( )
{
2024-11-26 20:03:10 +01:00
Flag = new FollowupFlag ( ) { FlagStatus = item . IsFlagged ? FollowupFlagStatus . Flagged : FollowupFlagStatus . NotFlagged }
2024-04-18 01:44:37 +02:00
} ;
2025-02-14 01:43:52 +01:00
return _graphClient . Me . Messages [ item . Item . Id ] . ToPatchRequestInformation ( message ) ;
2024-04-18 01:44:37 +02:00
} ) ;
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < RequestInformation > > MarkRead ( BatchMarkReadRequest request )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
return ForEachRequest ( request , ( item ) = >
2024-04-18 01:44:37 +02:00
{
var message = new Message ( )
{
2024-11-26 20:03:10 +01:00
IsRead = item . IsRead
2024-04-18 01:44:37 +02:00
} ;
return _graphClient . Me . Messages [ item . Item . Id ] . ToPatchRequestInformation ( message ) ;
} ) ;
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < RequestInformation > > Delete ( BatchDeleteRequest request )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
return ForEachRequest ( request , ( item ) = >
2024-04-18 01:44:37 +02:00
{
return _graphClient . Me . Messages [ item . Item . Id ] . ToDeleteRequestInformation ( ) ;
} ) ;
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < RequestInformation > > MoveToFocused ( BatchMoveToFocusedRequest request )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
return ForEachRequest ( request , ( item ) = >
2024-04-18 01:44:37 +02:00
{
if ( item is MoveToFocusedRequest moveToFocusedRequest )
{
var message = new Message ( )
{
InferenceClassification = moveToFocusedRequest . MoveToFocused ? InferenceClassificationType . Focused : InferenceClassificationType . Other
} ;
return _graphClient . Me . Messages [ moveToFocusedRequest . Item . Id ] . ToPatchRequestInformation ( message ) ;
}
throw new Exception ( "Invalid request type." ) ;
} ) ;
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < RequestInformation > > AlwaysMoveTo ( BatchAlwaysMoveToRequest request )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
return ForEachRequest ( request , ( item ) = >
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
var inferenceClassificationOverride = new InferenceClassificationOverride
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
ClassifyAs = item . MoveToFocused ? InferenceClassificationType . Focused : InferenceClassificationType . Other ,
SenderEmailAddress = new EmailAddress
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
Name = item . Item . FromName ,
Address = item . Item . FromAddress
}
} ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
return _graphClient . Me . InferenceClassification . Overrides . ToPostRequestInformation ( inferenceClassificationOverride ) ;
2024-04-18 01:44:37 +02:00
} ) ;
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < RequestInformation > > CreateDraft ( CreateDraftRequest createDraftRequest )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
var reason = createDraftRequest . DraftPreperationRequest . Reason ;
var message = createDraftRequest . DraftPreperationRequest . CreatedLocalDraftMimeMessage . AsOutlookMessage ( true ) ;
if ( reason = = DraftCreationReason . Empty )
{
return [ new HttpRequestBundle < RequestInformation > ( _graphClient . Me . Messages . ToPostRequestInformation ( message ) , createDraftRequest ) ] ;
}
else if ( reason = = DraftCreationReason . Reply )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
return [ new HttpRequestBundle < RequestInformation > ( _graphClient . Me . Messages [ createDraftRequest . DraftPreperationRequest . ReferenceMailCopy . Id ] . CreateReply . ToPostRequestInformation ( new Microsoft . Graph . Me . Messages . Item . CreateReply . CreateReplyPostRequestBody ( )
2024-04-18 01:44:37 +02:00
{
2024-11-26 20:03:10 +01:00
Message = message
} ) , createDraftRequest ) ] ;
2024-04-18 01:44:37 +02:00
2024-11-26 20:03:10 +01:00
}
else if ( reason = = DraftCreationReason . ReplyAll )
{
return [ new HttpRequestBundle < RequestInformation > ( _graphClient . Me . Messages [ createDraftRequest . DraftPreperationRequest . ReferenceMailCopy . Id ] . CreateReplyAll . ToPostRequestInformation ( new Microsoft . Graph . Me . Messages . Item . CreateReplyAll . CreateReplyAllPostRequestBody ( )
{
Message = message
} ) , createDraftRequest ) ] ;
}
else if ( reason = = DraftCreationReason . Forward )
{
return [ new HttpRequestBundle < RequestInformation > ( _graphClient . Me . Messages [ createDraftRequest . DraftPreperationRequest . ReferenceMailCopy . Id ] . CreateForward . ToPostRequestInformation ( new Microsoft . Graph . Me . Messages . Item . CreateForward . CreateForwardPostRequestBody ( )
{
Message = message
} ) , createDraftRequest ) ] ;
}
else
{
throw new NotImplementedException ( "Draft creation reason is not implemented." ) ;
}
2024-04-18 01:44:37 +02:00
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < RequestInformation > > SendDraft ( SendDraftRequest request )
2024-06-11 14:16:57 +02:00
{
var sendDraftPreparationRequest = request . Request ;
// 1. Delete draft
// 2. Create new Message with new MIME.
// 3. Make sure that conversation id is tagged correctly for replies.
var mailCopyId = sendDraftPreparationRequest . MailItem . Id ;
var mimeMessage = sendDraftPreparationRequest . Mime ;
2024-08-18 22:45:23 +02:00
// Convert mime message to Outlook message.
// Outlook synchronizer does not send MIME messages directly anymore.
// Alias support is lacking with direct MIMEs.
// Therefore we convert the MIME message to Outlook message and use proper APIs.
2024-06-11 14:16:57 +02:00
2024-08-19 03:44:16 +02:00
var outlookMessage = mimeMessage . AsOutlookMessage ( false ) ;
2024-06-11 14:16:57 +02:00
2024-09-05 17:23:15 +02:00
// Create attachment requests.
// TODO: We need to support large file attachments with sessioned upload at some point.
var attachmentRequestList = CreateAttachmentUploadBundles ( mimeMessage , mailCopyId , request ) . ToList ( ) ;
2024-08-19 03:44:16 +02:00
// Update draft.
2024-06-11 14:16:57 +02:00
2024-08-19 03:44:16 +02:00
var patchDraftRequest = _graphClient . Me . Messages [ mailCopyId ] . ToPatchRequestInformation ( outlookMessage ) ;
var patchDraftRequestBundle = new HttpRequestBundle < RequestInformation > ( patchDraftRequest , request ) ;
// Send draft.
2024-08-21 13:15:50 +02:00
var sendDraftRequest = PreparePostRequestInformation ( _graphClient . Me . Messages [ mailCopyId ] . Send . ToPostRequestInformation ( ) ) ;
2024-08-19 19:02:33 +02:00
var sendDraftRequestBundle = new HttpRequestBundle < RequestInformation > ( sendDraftRequest , request ) ;
2024-08-19 03:44:16 +02:00
2024-09-05 17:23:15 +02:00
return [ . . attachmentRequestList , patchDraftRequestBundle , sendDraftRequestBundle ] ;
}
2024-11-26 20:03:10 +01:00
private List < IRequestBundle < RequestInformation > > CreateAttachmentUploadBundles ( MimeMessage mime , string mailCopyId , IRequestBase sourceRequest )
2024-09-05 17:23:15 +02:00
{
var allAttachments = new List < OutlookFileAttachment > ( ) ;
foreach ( var part in mime . BodyParts )
{
var isAttachmentOrInline = part . IsAttachment ? true : part . ContentDisposition ? . Disposition = = "inline" ;
if ( ! isAttachmentOrInline ) continue ;
using var memory = new MemoryStream ( ) ;
( ( MimePart ) part ) . Content . DecodeTo ( memory ) ;
var base64String = Convert . ToBase64String ( memory . ToArray ( ) ) ;
var attachment = new OutlookFileAttachment ( )
{
Base64EncodedContentBytes = base64String ,
FileName = part . ContentDisposition ? . FileName ? ? part . ContentType . Name ,
ContentId = part . ContentId ,
ContentType = part . ContentType . MimeType ,
IsInline = part . ContentDisposition ? . Disposition = = "inline"
} ;
allAttachments . Add ( attachment ) ;
}
2025-02-14 01:43:52 +01:00
static RequestInformation PrepareUploadAttachmentRequest ( RequestInformation requestInformation , OutlookFileAttachment outlookFileAttachment )
2024-09-05 17:23:15 +02:00
{
requestInformation . Headers . Clear ( ) ;
2025-02-14 01:43:52 +01:00
string contentJson = JsonSerializer . Serialize ( outlookFileAttachment , OutlookSynchronizerJsonContext . Default . OutlookFileAttachment ) ;
2024-09-05 17:23:15 +02:00
requestInformation . Content = new MemoryStream ( Encoding . UTF8 . GetBytes ( contentJson ) ) ;
requestInformation . HttpMethod = Method . POST ;
requestInformation . Headers . Add ( "Content-Type" , "application/json" ) ;
return requestInformation ;
}
2024-11-26 20:03:10 +01:00
var retList = new List < IRequestBundle < RequestInformation > > ( ) ;
2024-09-05 17:23:15 +02:00
// Prepare attachment upload requests.
2024-11-26 20:03:10 +01:00
foreach ( var attachment in allAttachments )
2024-09-05 17:23:15 +02:00
{
var emptyPostRequest = _graphClient . Me . Messages [ mailCopyId ] . Attachments . ToPostRequestInformation ( new Attachment ( ) ) ;
2024-11-26 20:03:10 +01:00
var modifiedAttachmentUploadRequest = PrepareUploadAttachmentRequest ( emptyPostRequest , attachment ) ;
2024-09-05 17:23:15 +02:00
2024-11-26 20:03:10 +01:00
var bundle = new HttpRequestBundle < RequestInformation > ( modifiedAttachmentUploadRequest , null ) ;
retList . Add ( bundle ) ;
}
return retList ;
2024-06-11 14:16:57 +02:00
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < RequestInformation > > Archive ( BatchArchiveRequest request )
{
var batchMoveRequest = new BatchMoveRequest ( request . Select ( item = > new MoveRequest ( item . Item , item . FromFolder , item . ToFolder ) ) ) ;
return Move ( batchMoveRequest ) ;
}
2024-06-21 23:48:03 +02:00
2024-04-18 01:44:37 +02:00
public override async Task DownloadMissingMimeMessageAsync ( IMailItem mailItem ,
MailKit . ITransferProgress transferProgress = null ,
CancellationToken cancellationToken = default )
{
var mimeMessage = await DownloadMimeMessageAsync ( mailItem . Id , cancellationToken ) . ConfigureAwait ( false ) ;
await _outlookChangeProcessor . SaveMimeFileAsync ( mailItem . FileId , mimeMessage , Account . Id ) . ConfigureAwait ( false ) ;
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < RequestInformation > > RenameFolder ( RenameFolderRequest request )
2024-07-09 01:05:16 +02:00
{
2024-11-26 20:03:10 +01:00
var requestBody = new MailFolder
2024-07-09 01:05:16 +02:00
{
2024-11-26 20:03:10 +01:00
DisplayName = request . NewFolderName ,
} ;
2024-07-09 01:05:16 +02:00
2024-11-26 20:03:10 +01:00
var networkCall = _graphClient . Me . MailFolders [ request . Folder . RemoteFolderId ] . ToPatchRequestInformation ( requestBody ) ;
2024-07-09 01:05:16 +02:00
2024-11-26 20:03:10 +01:00
return [ new HttpRequestBundle < RequestInformation > ( networkCall , request ) ] ;
2024-07-09 01:05:16 +02:00
}
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < RequestInformation > > EmptyFolder ( EmptyFolderRequest request )
2024-07-09 01:05:16 +02:00
= > Delete ( new BatchDeleteRequest ( request . MailsToDelete . Select ( a = > new DeleteRequest ( a ) ) ) ) ;
2024-11-26 20:03:10 +01:00
public override List < IRequestBundle < RequestInformation > > 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
2024-11-26 20:03:10 +01:00
public override async Task ExecuteNativeRequestsAsync ( List < IRequestBundle < RequestInformation > > batchedRequests , CancellationToken cancellationToken = default )
2024-04-18 01:44:37 +02:00
{
2024-11-10 23:28:25 +01:00
var batchRequestInformations = batchedRequests . Batch ( ( int ) MaximumAllowedBatchRequestSize ) ;
2024-04-18 01:44:37 +02:00
2024-08-21 13:15:50 +02:00
bool serializeRequests = false ;
2024-04-18 01:44:37 +02:00
foreach ( var batch in batchRequestInformations )
{
var batchContent = new BatchRequestContentCollection ( _graphClient ) ;
var itemCount = batch . Count ( ) ;
for ( int i = 0 ; i < itemCount ; i + + )
{
var bundle = batch . ElementAt ( i ) ;
2024-12-22 00:49:55 +01:00
if ( bundle . UIChangeRequest is SendDraftRequest )
{
// This bundle needs to run every request in serial.
// By default requests are executed in parallel.
2024-08-21 13:15:50 +02:00
2024-12-22 00:49:55 +01:00
serializeRequests = true ;
}
2024-08-21 13:15:50 +02:00
2024-04-18 01:44:37 +02:00
var nativeRequest = bundle . NativeRequest ;
2024-11-26 20:03:10 +01:00
bundle . UIChangeRequest ? . ApplyUIChanges ( ) ;
2024-04-18 01:44:37 +02:00
2024-08-19 03:44:16 +02:00
var batchRequestId = await batchContent . AddBatchRequestStepAsync ( nativeRequest ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
// Map BundleId to batch request step's key.
// This is how we can identify which step succeeded or failed in the bundle.
2024-08-19 19:02:33 +02:00
bundle . BundleId = batchRequestId ;
2024-04-18 01:44:37 +02:00
}
if ( ! batchContent . BatchRequestSteps . Any ( ) )
continue ;
2024-08-19 03:44:16 +02:00
// Set execution type to serial instead of parallel if needed.
// Each step will depend on the previous one.
2024-08-21 13:15:50 +02:00
if ( serializeRequests )
2024-08-19 03:44:16 +02:00
{
for ( int i = 1 ; i < itemCount ; i + + )
{
var currentStep = batchContent . BatchRequestSteps . ElementAt ( i ) ;
var previousStep = batchContent . BatchRequestSteps . ElementAt ( i - 1 ) ;
currentStep . Value . DependsOn = [ previousStep . Key ] ;
}
}
2024-04-18 01:44:37 +02:00
// Execute batch. This will collect responses from network call for each batch step.
2024-08-19 19:02:33 +02:00
var batchRequestResponse = await _graphClient . Batch . PostAsync ( batchContent , cancellationToken ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
// Check responses for each bundle id.
// Each bundle id must return some HttpResponseMessage ideally.
var bundleIds = batchContent . BatchRequestSteps . Select ( a = > a . Key ) ;
2024-08-19 19:02:33 +02:00
var exceptionBag = new List < string > ( ) ;
2024-04-18 01:44:37 +02:00
foreach ( var bundleId in bundleIds )
{
var bundle = batch . FirstOrDefault ( a = > a . BundleId = = bundleId ) ;
if ( bundle = = null )
continue ;
var httpResponseMessage = await batchRequestResponse . GetResponseByIdAsync ( bundleId ) ;
2024-08-19 19:02:33 +02:00
if ( httpResponseMessage = = null )
continue ;
2024-04-18 01:44:37 +02:00
using ( httpResponseMessage )
{
2024-08-19 19:02:33 +02:00
if ( ! httpResponseMessage . IsSuccessStatusCode )
{
2024-11-26 20:03:10 +01:00
bundle . UIChangeRequest ? . RevertUIChanges ( ) ;
2024-08-21 13:15:50 +02:00
2024-08-19 19:02:33 +02:00
var content = await httpResponseMessage . Content . ReadAsStringAsync ( ) ;
2024-11-10 23:28:25 +01:00
var errorJson = JsonNode . Parse ( content ) ;
2024-12-22 00:49:55 +01:00
var errorString = $"[{httpResponseMessage.StatusCode}] {errorJson[" error "][" code "]} - {errorJson[" error "][" message "]}\n" ;
Debug . WriteLine ( errorString ) ;
2024-08-19 19:02:33 +02:00
exceptionBag . Add ( errorString ) ;
}
2024-04-18 01:44:37 +02:00
}
}
2024-08-19 19:02:33 +02:00
if ( exceptionBag . Any ( ) )
{
var formattedErrorString = string . Join ( "\n" , exceptionBag . Select ( ( item , index ) = > $"{index + 1}. {item}" ) ) ;
2024-04-18 01:44:37 +02:00
2024-08-19 19:02:33 +02:00
throw new SynchronizerException ( formattedErrorString ) ;
}
2024-04-18 01:44:37 +02:00
}
}
private async Task < MimeMessage > DownloadMimeMessageAsync ( string messageId , CancellationToken cancellationToken = default )
{
var mimeContentStream = await _graphClient . Me . Messages [ messageId ] . Content . GetAsync ( null , cancellationToken ) . ConfigureAwait ( false ) ;
return await MimeMessage . LoadAsync ( mimeContentStream ) . ConfigureAwait ( false ) ;
}
public override async Task < List < NewMailItemPackage > > CreateNewMailPackagesAsync ( Message message , MailItemFolder assignedFolder , CancellationToken cancellationToken = default )
{
2024-05-25 17:00:52 +02:00
var mimeMessage = await DownloadMimeMessageAsync ( message . Id , cancellationToken ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
var mailCopy = message . AsMailCopy ( ) ;
2024-06-11 22:48:18 +02:00
if ( message . IsDraft . GetValueOrDefault ( )
& & 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 isMappingSuccessful = await _outlookChangeProcessor . MapLocalDraftAsync ( Account . Id , localDraftCopyUniqueId , mailCopy . Id , mailCopy . DraftId , mailCopy . ThreadId ) ;
if ( isMappingSuccessful ) return null ;
// Local copy doesn't exists. Continue execution to insert mail copy.
}
// Outlook messages can only be assigned to 1 folder at a time.
// Therefore we don't need to create multiple copies of the same message for different folders.
var package = new NewMailItemPackage ( mailCopy , mimeMessage , assignedFolder . RemoteFolderId ) ;
return [ package ] ;
}
2024-12-24 18:30:25 +01:00
2025-01-06 02:15:21 +01:00
protected override async Task < CalendarSynchronizationResult > SynchronizeCalendarEventsInternalAsync ( CalendarSynchronizationOptions options , CancellationToken cancellationToken = default )
2024-12-24 18:30:25 +01:00
{
2025-01-06 02:15:21 +01:00
_logger . Information ( "Internal calendar synchronization started for {Name}" , Account . Name ) ;
cancellationToken . ThrowIfCancellationRequested ( ) ;
2025-01-16 22:00:05 +01:00
await SynchronizeCalendarsAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2025-01-06 02:15:21 +01:00
var localCalendars = await _outlookChangeProcessor . GetAccountCalendarsAsync ( Account . Id ) . ConfigureAwait ( false ) ;
2025-01-07 13:42:10 +01:00
Microsoft . Graph . Me . Calendars . Item . CalendarView . Delta . DeltaGetResponse eventsDeltaResponse = null ;
2025-01-06 21:56:33 +01:00
2025-01-07 13:42:10 +01:00
// TODO: Maybe we can batch each calendar?
2025-01-06 02:15:21 +01:00
2025-01-07 13:42:10 +01:00
foreach ( var calendar in localCalendars )
{
bool isInitialSync = string . IsNullOrEmpty ( calendar . SynchronizationDeltaToken ) ;
2025-01-06 02:15:21 +01:00
2025-01-07 13:42:10 +01:00
if ( isInitialSync )
2025-01-06 02:15:21 +01:00
{
2025-01-07 13:42:10 +01:00
_logger . Information ( "No calendar sync identifier for calendar {Name}. Performing initial sync." , calendar . Name ) ;
2025-01-06 02:15:21 +01:00
2025-01-07 13:42:10 +01:00
var startDate = DateTime . UtcNow . AddYears ( - 2 ) . ToString ( "u" ) ;
var endDate = DateTime . UtcNow . ToString ( "u" ) ;
2025-01-06 02:15:21 +01:00
2025-01-07 13:42:10 +01:00
eventsDeltaResponse = await _graphClient . Me . Calendars [ calendar . RemoteCalendarId ] . CalendarView . Delta . GetAsDeltaGetResponseAsync ( ( requestConfiguration ) = >
{
requestConfiguration . QueryParameters . StartDateTime = startDate ;
requestConfiguration . QueryParameters . EndDateTime = endDate ;
} , cancellationToken : cancellationToken ) ;
2025-01-06 02:15:21 +01:00
2025-01-07 13:42:10 +01:00
// No delta link. Performing initial sync.
//eventsDeltaResponse = await _graphClient.Me.CalendarView.Delta.GetAsDeltaGetResponseAsync((requestConfiguration) =>
//{
// requestConfiguration.QueryParameters.StartDateTime = startDate;
// requestConfiguration.QueryParameters.EndDateTime = endDate;
2025-01-06 02:15:21 +01:00
2025-01-07 13:42:10 +01:00
// // TODO: Expand does not work.
// // https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/2358
2025-01-06 02:15:21 +01:00
2025-01-07 13:42:10 +01:00
// requestConfiguration.QueryParameters.Expand = new string[] { "calendar($select=name,id)" }; // Expand the calendar and select name and id. Customize as needed.
//}, cancellationToken: cancellationToken);
}
else
{
var currentDeltaToken = calendar . SynchronizationDeltaToken ;
2025-01-06 02:15:21 +01:00
2025-01-07 13:42:10 +01:00
_logger . Information ( "Performing delta sync for calendar {Name}." , calendar . Name ) ;
2025-01-06 02:15:21 +01:00
2025-01-07 13:42:10 +01:00
var requestInformation = _graphClient . Me . Calendars [ calendar . RemoteCalendarId ] . CalendarView . Delta . ToGetRequestInformation ( ( requestConfiguration ) = >
{
2025-01-06 02:15:21 +01:00
2025-01-07 13:42:10 +01:00
//requestConfiguration.QueryParameters.StartDateTime = startDate;
//requestConfiguration.QueryParameters.EndDateTime = endDate;
} ) ;
2025-01-06 02:15:21 +01:00
2025-01-07 13:42:10 +01:00
//var requestInformation = _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.ToGetRequestInformation((config) =>
//{
// config.QueryParameters.Top = (int)InitialMessageDownloadCountPerFolder;
// config.QueryParameters.Select = outlookMessageSelectParameters;
// config.QueryParameters.Orderby = ["receivedDateTime desc"];
//});
2025-01-06 02:15:21 +01:00
2025-01-07 13:42:10 +01:00
requestInformation . UrlTemplate = requestInformation . UrlTemplate . Insert ( requestInformation . UrlTemplate . Length - 1 , ",%24deltatoken" ) ;
requestInformation . QueryParameters . Add ( "%24deltatoken" , currentDeltaToken ) ;
eventsDeltaResponse = await _graphClient . RequestAdapter . SendAsync ( requestInformation , Microsoft . Graph . Me . Calendars . Item . CalendarView . Delta . DeltaGetResponse . CreateFromDiscriminatorValue ) ;
2025-01-06 21:56:33 +01:00
}
2025-01-07 13:42:10 +01:00
List < Event > events = new ( ) ;
// We must first save the parent recurring events to not lose exceptions.
// Therefore, order the existing items by their type and save the parent recurring events first.
var messageIteratorAsync = PageIterator < Event , Microsoft . Graph . Me . Calendars . Item . CalendarView . Delta . DeltaGetResponse > . CreatePageIterator ( _graphClient , eventsDeltaResponse , ( item ) = >
{
events . Add ( item ) ;
return true ;
} ) ;
await messageIteratorAsync
. IterateAsync ( cancellationToken )
. ConfigureAwait ( false ) ;
// Desc-order will move parent recurring events to the top.
events = events . OrderByDescending ( a = > a . Type ) . ToList ( ) ;
_logger . Information ( "Found {Count} events in total." , events . Count ) ;
foreach ( var item in events )
2025-01-06 21:56:33 +01:00
{
2025-01-07 13:42:10 +01:00
try
{
await _handleItemRetrievalSemaphore . WaitAsync ( ) ;
await _outlookChangeProcessor . ManageCalendarEventAsync ( item , calendar , Account ) . ConfigureAwait ( false ) ;
}
2025-02-14 01:43:52 +01:00
catch ( Exception )
2025-01-07 13:42:10 +01:00
{
// _logger.Error(ex, "Error occurred while handling item {Id} for calendar {Name}", item.Id, calendar.Name);
}
finally
{
_handleItemRetrievalSemaphore . Release ( ) ;
}
2025-01-06 21:56:33 +01:00
}
2025-01-07 13:42:10 +01:00
var latestDeltaLink = messageIteratorAsync . Deltalink ;
//Store delta link for tracking new changes.
if ( ! string . IsNullOrEmpty ( latestDeltaLink ) )
2025-01-06 21:56:33 +01:00
{
2025-01-07 13:42:10 +01:00
// Parse Delta Token from Delta Link since v5 of Graph SDK works based on the token, not the link.
var deltaToken = GetDeltaTokenFromDeltaLink ( latestDeltaLink ) ;
await _outlookChangeProcessor . UpdateCalendarDeltaSynchronizationToken ( calendar . Id , deltaToken ) . ConfigureAwait ( false ) ;
2025-01-06 02:15:21 +01:00
}
}
return default ;
}
private async Task SynchronizeCalendarsAsync ( CancellationToken cancellationToken = default )
{
var calendars = await _graphClient . Me . Calendars . GetAsync ( cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
var localCalendars = await _outlookChangeProcessor . GetAccountCalendarsAsync ( Account . Id ) . ConfigureAwait ( false ) ;
List < AccountCalendar > insertedCalendars = new ( ) ;
List < AccountCalendar > updatedCalendars = new ( ) ;
List < AccountCalendar > deletedCalendars = new ( ) ;
// 1. Handle deleted calendars.
foreach ( var calendar in localCalendars )
{
var remoteCalendar = calendars . Value . FirstOrDefault ( a = > a . Id = = calendar . RemoteCalendarId ) ;
if ( remoteCalendar = = null )
{
// Local calendar doesn't exists remotely. Delete local copy.
await _outlookChangeProcessor . DeleteAccountCalendarAsync ( calendar ) . ConfigureAwait ( false ) ;
deletedCalendars . Add ( calendar ) ;
}
}
// Delete the deleted folders from local list.
deletedCalendars . ForEach ( a = > localCalendars . Remove ( a ) ) ;
// 2. Handle update/insert based on remote calendars.
foreach ( var calendar in calendars . Value )
{
var existingLocalCalendar = localCalendars . FirstOrDefault ( a = > a . RemoteCalendarId = = calendar . Id ) ;
if ( existingLocalCalendar = = null )
{
// Insert new calendar.
var localCalendar = calendar . AsCalendar ( Account ) ;
insertedCalendars . Add ( localCalendar ) ;
}
else
{
// Update existing calendar. Right now we only update the name.
if ( ShouldUpdateCalendar ( calendar , existingLocalCalendar ) )
{
existingLocalCalendar . Name = calendar . Name ;
updatedCalendars . Add ( existingLocalCalendar ) ;
}
else
{
// Remove it from the local folder list to skip additional calendar updates.
localCalendars . Remove ( existingLocalCalendar ) ;
}
}
}
// 3.Process changes in order-> Insert, Update. Deleted ones are already processed.
foreach ( var calendar in insertedCalendars )
{
await _outlookChangeProcessor . InsertAccountCalendarAsync ( calendar ) . ConfigureAwait ( false ) ;
}
foreach ( var calendar in updatedCalendars )
{
await _outlookChangeProcessor . UpdateAccountCalendarAsync ( calendar ) . ConfigureAwait ( false ) ;
}
if ( insertedCalendars . Any ( ) | | deletedCalendars . Any ( ) | | updatedCalendars . Any ( ) )
{
// TODO: Notify calendar updates.
// WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id));
}
}
private bool ShouldUpdateCalendar ( Calendar calendar , AccountCalendar accountCalendar )
{
// TODO: Only calendar name is updated for now. We can add more checks here.
var remoteCalendarName = calendar . Name ;
var localCalendarName = accountCalendar . Name ;
return ! localCalendarName . Equals ( remoteCalendarName , StringComparison . OrdinalIgnoreCase ) ;
2024-12-24 18:30:25 +01:00
}
2025-02-15 12:53:32 +01:00
public override async Task KillSynchronizerAsync ( )
{
await base . KillSynchronizerAsync ( ) ;
_graphClient . Dispose ( ) ;
}
2024-04-18 01:44:37 +02:00
}
}