Improving thread mapping for all synchronizers.

This commit is contained in:
Burak Kaan Köse
2026-02-23 01:51:44 +01:00
parent c5a631da6f
commit 79a81710f0
7 changed files with 314 additions and 60 deletions
@@ -438,7 +438,9 @@ public static class OutlookIntegratorExtensions
private static List<InternetMessageHeader> GetHeaderList(this MimeMessage mime)
{
// Graph API only allows max of 5 headers.
// Prioritize threading headers to keep reply grouping intact.
// Graph only allows setting custom internet headers (typically X-*).
// Reply/threading headers like In-Reply-To and References are managed by
// createReply/createReplyAll flows and must not be sent here.
const int headerLimit = 5;
string[] headersToIgnore = ["Date", "To", "Cc", "Bcc", "MIME-Version", "From", "Subject", "Message-Id"];
@@ -461,18 +463,12 @@ public static class OutlookIntegratorExtensions
if (winoDraftHeader != null)
AddHeader(winoDraftHeader.Field, winoDraftHeader.Value);
// Threading headers must be preserved with their real RFC names.
AddHeader("In-Reply-To", mime.Headers[HeaderId.InReplyTo]);
AddHeader("References", mime.Headers[HeaderId.References]);
// Fill remaining slots with custom headers only (avoid Graph restrictions).
foreach (var header in mime.Headers)
{
if (headers.Count >= headerLimit) break;
if (header.Field == Domain.Constants.WinoLocalDraftHeader) continue;
if (headersToIgnore.Contains(header.Field)) continue;
if (string.Equals(header.Field, "In-Reply-To", StringComparison.OrdinalIgnoreCase)) continue;
if (string.Equals(header.Field, "References", StringComparison.OrdinalIgnoreCase)) continue;
// Only include custom headers beyond the core threading ones.
if (!header.Field.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) continue;
@@ -1183,6 +1183,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
message.ThreadId = singleDraftRequest.Item.ThreadId;
}
// Local draft mapping header must never leak to recipients.
singleDraftRequest.Request.Mime.Headers.Remove(Domain.Constants.WinoLocalDraftHeader);
singleDraftRequest.Request.Mime.Prepare(EncodingConstraint.None);
var mimeString = singleDraftRequest.Request.Mime.ToString();
@@ -1812,6 +1812,10 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
{
await HandleFailedResponseAsync(bundle, response, errors);
}
else
{
await HandleSuccessfulResponseAsync(bundle, response).ConfigureAwait(false);
}
}
}
@@ -1859,6 +1863,39 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
}
}
private async Task HandleSuccessfulResponseAsync(IRequestBundle<RequestInformation> bundle, HttpResponseMessage response)
{
if (bundle?.UIChangeRequest is not CreateDraftRequest createDraftRequest)
return;
try
{
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(content))
return;
var json = JsonNode.Parse(content);
var createdDraftId = json?["id"]?.GetValue<string>();
if (string.IsNullOrWhiteSpace(createdDraftId))
return;
var createdConversationId = json?["conversationId"]?.GetValue<string>();
var localDraft = createDraftRequest.DraftPreperationRequest.CreatedLocalDraftCopy;
await _outlookChangeProcessor.MapLocalDraftAsync(
Account.Id,
localDraft.UniqueId,
createdDraftId,
createdConversationId,
createdConversationId).ConfigureAwait(false);
}
catch (Exception ex)
{
// Draft mapping is best-effort here. Delta sync mapping remains as fallback.
_logger.Debug(ex, "Failed to map Outlook draft from create-draft response.");
}
}
private void ThrowBatchExecutionException(List<string> errors)
{
var formattedErrorString = string.Join("\n",