diff --git a/Wino.Core.Domain/Extensions/MailHeaderExtensions.cs b/Wino.Core.Domain/Extensions/MailHeaderExtensions.cs
new file mode 100644
index 00000000..aa41729b
--- /dev/null
+++ b/Wino.Core.Domain/Extensions/MailHeaderExtensions.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Linq;
+
+namespace Wino.Core.Domain.Extensions;
+
+public static class MailHeaderExtensions
+{
+ ///
+ /// Strips angle brackets from a Message-ID or In-Reply-To value.
+ /// RFC 5322 Message-IDs are formatted as <id@domain>, but MimeKit
+ /// properties store them without brackets. This normalizes raw header
+ /// values to match MimeKit's convention.
+ ///
+ public static string StripAngleBrackets(string value)
+ {
+ if (string.IsNullOrEmpty(value)) return value;
+
+ value = value.Trim();
+
+ if (value.StartsWith("<") && value.EndsWith(">"))
+ return value.Substring(1, value.Length - 2);
+
+ return value;
+ }
+
+ ///
+ /// Normalizes a raw RFC References header value into semicolon-separated Message-IDs
+ /// without angle brackets. Raw References headers contain space-separated bracketed IDs
+ /// like "<id1@domain> <id2@domain>". This converts them to "id1@domain;id2@domain".
+ ///
+ public static string NormalizeReferences(string rawReferences)
+ {
+ if (string.IsNullOrEmpty(rawReferences)) return rawReferences;
+
+ var ids = rawReferences
+ .Split(new[] { ' ', '\t', '\r', '\n', ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(StripAngleBrackets)
+ .Where(id => !string.IsNullOrEmpty(id));
+
+ return string.Join(";", ids);
+ }
+}
diff --git a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs
index 5036f2b8..894b1bce 100644
--- a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs
+++ b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs
@@ -8,6 +8,7 @@ using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Extensions;
using Wino.Core.Misc;
namespace Wino.Core.Extensions;
@@ -64,6 +65,20 @@ public static class OutlookIntegratorExtensions
ItemType = MailItemType.Mail // ItemType will be set by caller if calendar access is granted
};
+ // Extract In-Reply-To and References from InternetMessageHeaders for threading.
+ if (outlookMessage.InternetMessageHeaders != null)
+ {
+ var inReplyToHeader = outlookMessage.InternetMessageHeaders
+ .FirstOrDefault(h => string.Equals(h.Name, "In-Reply-To", StringComparison.OrdinalIgnoreCase));
+ if (inReplyToHeader != null)
+ mailCopy.InReplyTo = MailHeaderExtensions.StripAngleBrackets(inReplyToHeader.Value);
+
+ var referencesHeader = outlookMessage.InternetMessageHeaders
+ .FirstOrDefault(h => string.Equals(h.Name, "References", StringComparison.OrdinalIgnoreCase));
+ if (referencesHeader != null)
+ mailCopy.References = MailHeaderExtensions.NormalizeReferences(referencesHeader.Value);
+ }
+
if (mailCopy.IsDraft)
mailCopy.DraftId = mailCopy.ThreadId;
@@ -134,7 +149,7 @@ public static class OutlookIntegratorExtensions
CcRecipients = ccAddresses,
BccRecipients = bccAddresses,
From = fromAddress,
- InternetMessageId = GetProperId(mime.MessageId),
+ InternetMessageId = mime.MessageId,
ReplyTo = replyToAddresses,
Attachments = []
};
@@ -429,16 +444,6 @@ public static class OutlookIntegratorExtensions
return headers;
}
- private static string GetProperId(string id)
- {
- // Outlook requires some identifiers to start with "X-" or "x-".
- if (string.IsNullOrEmpty(id)) return string.Empty;
-
- if (!id.StartsWith("x-") || !id.StartsWith("X-"))
- return $"X-{id}";
-
- return id;
- }
#endregion
diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs
index b390a9a6..74023de5 100644
--- a/Wino.Core/Synchronizers/GmailSynchronizer.cs
+++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs
@@ -25,6 +25,7 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
+using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Folders;
@@ -1691,9 +1692,9 @@ public class GmailSynchronizer : WinoSynchronizer h.Name.Equals("In-Reply-To", StringComparison.OrdinalIgnoreCase))?.Value,
- MessageId = gmailMessage.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals("Message-Id", StringComparison.OrdinalIgnoreCase))?.Value,
- References = gmailMessage.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals("References", StringComparison.OrdinalIgnoreCase))?.Value,
+ InReplyTo = MailHeaderExtensions.StripAngleBrackets(gmailMessage.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals("In-Reply-To", StringComparison.OrdinalIgnoreCase))?.Value),
+ MessageId = MailHeaderExtensions.StripAngleBrackets(gmailMessage.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals("Message-Id", StringComparison.OrdinalIgnoreCase))?.Value),
+ References = MailHeaderExtensions.NormalizeReferences(gmailMessage.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals("References", StringComparison.OrdinalIgnoreCase))?.Value),
FileId = Guid.NewGuid(),
ItemType = itemType
};
diff --git a/Wino.Core/Synchronizers/ImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSynchronizer.cs
index 2fc17dbf..1ac92063 100644
--- a/Wino.Core/Synchronizers/ImapSynchronizer.cs
+++ b/Wino.Core/Synchronizers/ImapSynchronizer.cs
@@ -196,6 +196,9 @@ public class ImapSynchronizer : WinoSynchronizer();
- if (!string.IsNullOrEmpty(draftCreationOptions.ReferencedMessage.MimeMessage.MessageId))
- copy.InReplyTo = draftCreationOptions.ReferencedMessage.MimeMessage.MessageId;
+ if (refMime.References != null)
+ refs.AddRange(refMime.References);
+
+ if (!string.IsNullOrEmpty(refMime.MessageId))
+ {
+ copy.InReplyTo = refMime.MessageId;
+ refs.Add(refMime.MessageId);
+ }
+
+ if (refs.Count > 0)
+ copy.References = string.Join(";", refs);
if (!string.IsNullOrEmpty(draftCreationOptions.ReferencedMessage.MailCopy?.ThreadId))
copy.ThreadId = draftCreationOptions.ReferencedMessage.MailCopy.ThreadId;
@@ -960,8 +970,8 @@ public class MailService : BaseDatabaseService, IMailService
if (!string.IsNullOrEmpty(referenceMailCopy.References))
{
- // Parse the References string and add them
- var references = referenceMailCopy.References.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+ // Parse the References string (supports both ";" and "," separators for backward compatibility)
+ var references = referenceMailCopy.References.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var reference in references)
{
message.References.Add(reference.Trim());