Merge read receipt tracking work
This commit is contained in:
@@ -2133,6 +2133,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
IsDraft = isDraft,
|
||||
HasAttachments = gmailMessage.Payload?.Parts?.Any(p => !string.IsNullOrEmpty(p.Filename)) ?? false,
|
||||
IsRead = !isUnread,
|
||||
IsReadReceiptRequested = HasReadReceiptRequest(gmailMessage.Payload?.Headers),
|
||||
IsFlagged = isFlagged,
|
||||
IsFocused = isFocused,
|
||||
InReplyTo = MailHeaderExtensions.StripAngleBrackets(gmailMessage.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals("In-Reply-To", StringComparison.OrdinalIgnoreCase))?.Value),
|
||||
@@ -2181,6 +2182,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
if (string.IsNullOrEmpty(copy.MessageId))
|
||||
copy.MessageId = MailHeaderExtensions.NormalizeMessageId(mime.Headers[HeaderId.MessageId]);
|
||||
|
||||
if (!copy.IsReadReceiptRequested)
|
||||
copy.IsReadReceiptRequested = mime.HasReadReceiptRequest();
|
||||
|
||||
if (string.IsNullOrEmpty(copy.InReplyTo))
|
||||
copy.InReplyTo = MailHeaderExtensions.NormalizeMessageId(mime.InReplyTo);
|
||||
|
||||
@@ -2264,6 +2268,17 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
return ("", emailOnly);
|
||||
}
|
||||
|
||||
private static bool HasReadReceiptRequest(IList<MessagePartHeader> headers)
|
||||
=> headers?.Any(h => h.Name.Equals(Domain.Constants.DispositionNotificationToHeader, StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrWhiteSpace(h.Value)) == true;
|
||||
|
||||
private static bool LooksLikeReadReceipt(IList<MessagePartHeader> headers)
|
||||
{
|
||||
var contentType = headers?.FirstOrDefault(h => h.Name.Equals("Content-Type", StringComparison.OrdinalIgnoreCase))?.Value;
|
||||
return !string.IsNullOrWhiteSpace(contentType)
|
||||
&& contentType.Contains("disposition-notification", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AccountContact> ExtractContactsFromGmailMessage(Message message, MimeMessage mimeMessage)
|
||||
{
|
||||
var contacts = new Dictionary<string, AccountContact>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -2381,7 +2396,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
|
||||
// Initial sync metadata flow does not include MIME, but calendar invitations need MIME
|
||||
// for date rendering and invitation-to-calendar mapping.
|
||||
if (mimeMessage == null && baseMailCopy?.ItemType == MailItemType.CalendarInvitation && !string.IsNullOrEmpty(message?.Id))
|
||||
if (mimeMessage == null &&
|
||||
(baseMailCopy?.ItemType == MailItemType.CalendarInvitation || LooksLikeReadReceipt(message?.Payload?.Headers)) &&
|
||||
!string.IsNullOrEmpty(message?.Id))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -2396,7 +2413,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Failed to fetch raw MIME for calendar invitation {MessageId}", message.Id);
|
||||
_logger.Warning(ex, "Failed to fetch raw MIME for Gmail message {MessageId}", message.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
"Flag",
|
||||
"Importance",
|
||||
"IsRead",
|
||||
"IsReadReceiptRequested",
|
||||
"IsDraft",
|
||||
"ReceivedDateTime",
|
||||
"HasAttachments",
|
||||
@@ -401,7 +402,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
{
|
||||
// For drafts and calendar invitations, download MIME during initial sync like delta sync.
|
||||
var itemType = Account.IsCalendarAccessGranted ? message.GetMailItemType() : MailItemType.Mail;
|
||||
if (folder.SpecialFolderType == SpecialFolderType.Draft || itemType == MailItemType.CalendarInvitation)
|
||||
if (ShouldDownloadMimeForMessage(message, folder, itemType))
|
||||
{
|
||||
var draftPackages = await CreateNewMailPackagesAsync(message, folder, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -592,24 +593,47 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
|
||||
if (message != null)
|
||||
{
|
||||
// Create MailCopy from metadata only
|
||||
var mailCopy = await CreateMailCopyFromMessageAsync(message, folder).ConfigureAwait(false);
|
||||
var itemType = Account.IsCalendarAccessGranted ? message.GetMailItemType() : MailItemType.Mail;
|
||||
|
||||
if (mailCopy != null)
|
||||
if (ShouldDownloadMimeForMessage(message, folder, itemType))
|
||||
{
|
||||
// Create package without MIME
|
||||
var contacts = ExtractContactsFromOutlookMessage(message);
|
||||
var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId, contacts);
|
||||
bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
|
||||
var packages = await CreateNewMailPackagesAsync(message, folder, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (isInserted)
|
||||
if (packages != null)
|
||||
{
|
||||
downloadedIds.Add(mailCopy.Id);
|
||||
_logger.Debug("Downloaded metadata for message {MailId} in folder {FolderName}", messageId, folder.FolderName);
|
||||
foreach (var package in packages)
|
||||
{
|
||||
bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
|
||||
|
||||
if (isInserted)
|
||||
{
|
||||
downloadedIds.Add(package.Copy.Id);
|
||||
_logger.Debug("Downloaded MIME-backed message {MailId} in folder {FolderName}", messageId, folder.FolderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create MailCopy from metadata only
|
||||
var mailCopy = await CreateMailCopyFromMessageAsync(message, folder).ConfigureAwait(false);
|
||||
|
||||
if (mailCopy != null)
|
||||
{
|
||||
_logger.Warning("Failed to insert mail {MailId} for folder {FolderName}", messageId, folder.FolderName);
|
||||
// Create package without MIME
|
||||
var contacts = ExtractContactsFromOutlookMessage(message);
|
||||
var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId, contacts);
|
||||
bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
|
||||
|
||||
if (isInserted)
|
||||
{
|
||||
downloadedIds.Add(mailCopy.Id);
|
||||
_logger.Debug("Downloaded metadata for message {MailId} in folder {FolderName}", messageId, folder.FolderName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warning("Failed to insert mail {MailId} for folder {FolderName}", messageId, folder.FolderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -685,6 +709,21 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
/// Creates a MailCopy from an Outlook Message with metadata only (centralized method).
|
||||
/// This replaces the scattered CreateMinimalMailCopyAsync and AsMailCopy calls.
|
||||
/// </summary>
|
||||
private static bool ShouldDownloadMimeForMessage(Message message, MailItemFolder folder, MailItemType itemType)
|
||||
=> folder.SpecialFolderType == SpecialFolderType.Draft
|
||||
|| itemType == MailItemType.CalendarInvitation
|
||||
|| LooksLikeReadReceipt(message);
|
||||
|
||||
private static bool LooksLikeReadReceipt(Message message)
|
||||
{
|
||||
var contentType = message?.InternetMessageHeaders?
|
||||
.FirstOrDefault(h => string.Equals(h.Name, "Content-Type", StringComparison.OrdinalIgnoreCase))
|
||||
?.Value;
|
||||
|
||||
return !string.IsNullOrWhiteSpace(contentType)
|
||||
&& contentType.Contains("disposition-notification", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task<MailCopy> CreateMailCopyFromMessageAsync(Message message, MailItemFolder assignedFolder)
|
||||
{
|
||||
if (message == null) return null;
|
||||
|
||||
Reference in New Issue
Block a user