Threading improvements.
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Wino.Core.Domain.Extensions;
|
||||
|
||||
public static class MailHeaderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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".
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<IClientServiceRequest, Message
|
||||
IsRead = !isUnread,
|
||||
IsFlagged = isFlagged,
|
||||
IsFocused = isFocused,
|
||||
InReplyTo = gmailMessage.Payload?.Headers?.FirstOrDefault(h => 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
|
||||
};
|
||||
|
||||
@@ -196,6 +196,9 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
if (!smtpClient.IsAuthenticated)
|
||||
await smtpClient.AuthenticateAsync(Account.ServerInformation.OutgoingServerUsername, Account.ServerInformation.OutgoingServerPassword);
|
||||
|
||||
// Remove local draft header before sending to prevent leaking to recipients.
|
||||
singleRequest.Mime.Headers.Remove(Domain.Constants.WinoLocalDraftHeader);
|
||||
|
||||
// TODO: Transfer progress implementation as popup in the UI.
|
||||
await smtpClient.SendAsync(singleRequest.Mime, default);
|
||||
await smtpClient.DisconnectAsync(true);
|
||||
@@ -218,10 +221,6 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
var sentFolder = await client.GetFolderAsync(singleRequest.SentFolder.RemoteFolderId);
|
||||
|
||||
await sentFolder.OpenAsync(FolderAccess.ReadWrite);
|
||||
|
||||
// Delete local Wino draft header. Otherwise mapping will be applied on re-sync.
|
||||
singleRequest.Mime.Headers.Remove(Domain.Constants.WinoLocalDraftHeader);
|
||||
|
||||
await sentFolder.AppendAsync(singleRequest.Mime, MessageFlags.Seen);
|
||||
await sentFolder.CloseAsync();
|
||||
}
|
||||
|
||||
@@ -86,14 +86,24 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
FileId = Guid.NewGuid()
|
||||
};
|
||||
|
||||
// If replying, add In-Reply-To, ThreadId and References.
|
||||
// If replying, add In-Reply-To, ThreadId and References per RFC 5322.
|
||||
// References must include all previous References + the Message-ID of the message being replied to.
|
||||
if (draftCreationOptions.ReferencedMessage != null)
|
||||
{
|
||||
if (draftCreationOptions.ReferencedMessage.MimeMessage.References != null)
|
||||
copy.References = string.Join(",", draftCreationOptions.ReferencedMessage.MimeMessage.References);
|
||||
var refMime = draftCreationOptions.ReferencedMessage.MimeMessage;
|
||||
var refs = new List<string>();
|
||||
|
||||
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());
|
||||
|
||||
Reference in New Issue
Block a user