Addressing some Outlook sending issues due to API delay.

This commit is contained in:
Burak Kaan Köse
2024-08-21 13:15:50 +02:00
parent 16feb8602d
commit 0baac3dc49
10 changed files with 133 additions and 81 deletions

View File

@@ -206,12 +206,18 @@ namespace Wino.Core.Synchronizers
}
// Let servers to finish their job. Sometimes the servers doesn't respond immediately.
// Bug: if Outlook can't create the message in Sent Items folder before this delay,
// message will not appear in user's inbox since it's not in the Sent Items folder.
bool shouldDelayExecution = batches.Any(a => a.DelayExecution);
bool shouldDelayExecution =
(Account.ProviderType == MailProviderType.Outlook || Account.ProviderType == MailProviderType.Office365)
&& batches.Any(a => a.ResynchronizationDelay > 0);
if (shouldDelayExecution)
{
await Task.Delay(2000);
var maxDelay = batches.Aggregate(0, (max, next) => Math.Max(max, next.ResynchronizationDelay));
await Task.Delay(maxDelay);
}
// Start the internal synchronization.

View File

@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading;
@@ -145,8 +146,7 @@ namespace Wino.Core.Synchronizers
{
var synchronizationFolders = await _outlookChangeProcessor.GetSynchronizationFoldersAsync(options).ConfigureAwait(false);
_logger.Information("Found {Count} folders to synchronize.", synchronizationFolders.Count);
_logger.Information(string.Format("Folders: {0}", string.Join(",", synchronizationFolders.Select(a => a.FolderName))));
_logger.Information(string.Format("{1} Folders: {0}", string.Join(",", synchronizationFolders.Select(a => a.FolderName)), synchronizationFolders.Count));
for (int i = 0; i < synchronizationFolders.Count; i++)
{
@@ -184,8 +184,6 @@ namespace Wino.Core.Synchronizers
{
var downloadedMessageIds = new List<string>();
_logger.Debug("Started synchronization for folder {FolderName}", folder.FolderName);
cancellationToken.ThrowIfCancellationRequested();
string latestDeltaLink = string.Empty;
@@ -194,6 +192,8 @@ namespace Wino.Core.Synchronizers
Microsoft.Graph.Me.MailFolders.Item.Messages.Delta.DeltaGetResponse messageCollectionPage = null;
_logger.Debug("Synchronizing {FolderName}", folder.FolderName);
if (isInitialSync)
{
_logger.Debug("No sync identifier for Folder {FolderName}. Performing initial sync.", folder.FolderName);
@@ -211,9 +211,6 @@ namespace Wino.Core.Synchronizers
{
var currentDeltaToken = folder.DeltaToken;
_logger.Debug("Sync identifier found for Folder {FolderName}. Performing delta sync.", folder.FolderName);
_logger.Debug("Current delta token: {CurrentDeltaToken}", currentDeltaToken);
var requestInformation = _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.ToGetRequestInformation((config) =>
{
config.QueryParameters.Top = (int)InitialMessageDownloadCountPerFolder;
@@ -257,9 +254,6 @@ namespace Wino.Core.Synchronizers
_logger.Debug("Downloaded {Count} messages for folder {FolderName}", downloadedMessageIds.Count, folder.FolderName);
}
_logger.Debug("Iterator completed for folder {FolderName}", folder.FolderName);
_logger.Debug("Extracted latest delta link is {LatestDeltaLink}", latestDeltaLink);
//Store delta link for tracking new changes.
if (!string.IsNullOrEmpty(latestDeltaLink))
{
@@ -507,6 +501,27 @@ namespace Wino.Core.Synchronizers
return new ProfileInformation(senderName, profilePictureData);
}
/// <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>
private RequestInformation PreparePostRequestInformation(RequestInformation requestInformation, object content = null)
{
requestInformation.Headers.Clear();
string contentJson = content == null ? "{}" : JsonSerializer.Serialize(content);
requestInformation.Content = new MemoryStream(Encoding.UTF8.GetBytes(contentJson));
requestInformation.HttpMethod = Method.POST;
requestInformation.Headers.Add("Content-Type", "application/json");
return requestInformation;
}
#region Mail Integration
public override bool DelaySendOperationSynchronization() => true;
@@ -520,7 +535,8 @@ namespace Wino.Core.Synchronizers
return CreateBatchedHttpBundle(request, (item) =>
{
return _graphClient.Me.Messages[item.Item.Id.ToString()].Move.ToPostRequestInformation(requestBody);
return PreparePostRequestInformation(_graphClient.Me.Messages[item.Item.Id.ToString()].Move.ToPostRequestInformation(requestBody),
requestBody);
});
}
@@ -665,20 +681,8 @@ namespace Wino.Core.Synchronizers
// Send draft.
// 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.
var sendDraftRequest = _graphClient.Me.Messages[mailCopyId].Send.ToPostRequestInformation((config) =>
{
config.Headers.Add("Content-Type", "application/json");
});
sendDraftRequest.Headers.Clear();
sendDraftRequest.Content = new MemoryStream(Encoding.UTF8.GetBytes("{}"));
sendDraftRequest.HttpMethod = Method.POST;
sendDraftRequest.Headers.Add("Content-Type", "application/json");
var sendDraftRequest = PreparePostRequestInformation(_graphClient.Me.Messages[mailCopyId].Send.ToPostRequestInformation());
var sendDraftRequestBundle = new HttpRequestBundle<RequestInformation>(sendDraftRequest, request);
@@ -724,6 +728,8 @@ namespace Wino.Core.Synchronizers
{
var batchRequestInformations = BatchExtension.Batch(batchedRequests, (int)MaximumAllowedBatchRequestSize);
bool serializeRequests = false;
foreach (var batch in batchRequestInformations)
{
var batchContent = new BatchRequestContentCollection(_graphClient);
@@ -734,6 +740,14 @@ namespace Wino.Core.Synchronizers
{
var bundle = batch.ElementAt(i);
if (bundle.Request is BatchRequestBase batchBundleRequest && batchBundleRequest.ExecuteSerialBatch)
{
// This bundle needs to run every request in serial.
// By default requests are executed in parallel.
serializeRequests = true;
}
var request = bundle.Request;
var nativeRequest = bundle.NativeRequest;
@@ -753,7 +767,7 @@ namespace Wino.Core.Synchronizers
// Set execution type to serial instead of parallel if needed.
// Each step will depend on the previous one.
if (itemCount > 1)
if (serializeRequests)
{
for (int i = 1; i < itemCount; i++)
{
@@ -762,7 +776,6 @@ namespace Wino.Core.Synchronizers
currentStep.Value.DependsOn = [previousStep.Key];
}
}
// Execute batch. This will collect responses from network call for each batch step.
@@ -791,9 +804,11 @@ namespace Wino.Core.Synchronizers
{
if (!httpResponseMessage.IsSuccessStatusCode)
{
bundle.Request.RevertUIChanges();
var content = await httpResponseMessage.Content.ReadAsStringAsync();
var errorJson = JsonObject.Parse(content);
var errorString = $"({httpResponseMessage.StatusCode}) {errorJson["error"]["code"]} - {errorJson["error"]["message"]}";
var errorString = $"{httpResponseMessage.StatusCode} [{bundle.Request.GetType().Name}]\n{errorJson["error"]["code"]} - {errorJson["error"]["message"]}\n";
exceptionBag.Add(errorString);
}
@@ -818,12 +833,6 @@ namespace Wino.Core.Synchronizers
public override async Task<List<NewMailItemPackage>> CreateNewMailPackagesAsync(Message message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default)
{
bool isMailExists = await _outlookChangeProcessor.IsMailExistsAsync(message.Id);
if (isMailExists)
{
return null;
}
var mimeMessage = await DownloadMimeMessageAsync(message.Id, cancellationToken).ConfigureAwait(false);
var mailCopy = message.AsMailCopy();