Merge read receipt tracking work
This commit is contained in:
@@ -68,6 +68,7 @@ public class DatabaseService : IDatabaseService
|
||||
Connection.CreateTableAsync<CalendarAttachment>(),
|
||||
Connection.CreateTableAsync<Reminder>(),
|
||||
Connection.CreateTableAsync<MailInvitationCalendarMapping>(),
|
||||
Connection.CreateTableAsync<SentMailReceiptState>(),
|
||||
Connection.CreateTableAsync<WinoAccount>());
|
||||
|
||||
await EnsureSchemaUpgradesAsync().ConfigureAwait(false);
|
||||
@@ -234,5 +235,8 @@ SET {nameof(KeyboardShortcut.Action)} =
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailInvitationCalendarMapping_InvitationUid ON MailInvitationCalendarMapping(InvitationUid)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailInvitationCalendarMapping_CalendarId ON MailInvitationCalendarMapping(CalendarId)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailInvitationCalendarMapping_CalendarItemId ON MailInvitationCalendarMapping(CalendarItemId)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_SentMailReceiptState_MailUniqueId ON SentMailReceiptState(MailUniqueId)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_SentMailReceiptState_AccountId_MessageId ON SentMailReceiptState(AccountId, MessageId)").ConfigureAwait(false);
|
||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_SentMailReceiptState_Status ON SentMailReceiptState(Status)").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,9 @@ public static class MailkitClientExtensions
|
||||
MessageId = messageId,
|
||||
Subject = subject,
|
||||
IsRead = messageSummary.Flags.GetIsRead(),
|
||||
IsReadReceiptRequested = mime?.HasReadReceiptRequest()
|
||||
?? (messageSummary.Headers?.Contains(Constants.DispositionNotificationToHeader) == true
|
||||
&& !string.IsNullOrWhiteSpace(messageSummary.Headers[Constants.DispositionNotificationToHeader])),
|
||||
IsFlagged = messageSummary.Flags.GetIsFlagged(),
|
||||
PreviewText = previewText,
|
||||
FromAddress = fromAddress,
|
||||
|
||||
@@ -31,6 +31,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
private readonly ISignatureService _signatureService;
|
||||
private readonly IMimeFileService _mimeFileService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private readonly ISentMailReceiptService _sentMailReceiptService;
|
||||
|
||||
private readonly ILogger _logger = Log.ForContext<MailService>();
|
||||
|
||||
@@ -40,7 +41,8 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
IAccountService accountService,
|
||||
ISignatureService signatureService,
|
||||
IMimeFileService mimeFileService,
|
||||
IPreferencesService preferencesService) : base(databaseService)
|
||||
IPreferencesService preferencesService,
|
||||
ISentMailReceiptService sentMailReceiptService) : base(databaseService)
|
||||
{
|
||||
_folderService = folderService;
|
||||
_contactService = contactService;
|
||||
@@ -48,6 +50,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
_signatureService = signatureService;
|
||||
_mimeFileService = mimeFileService;
|
||||
_preferencesService = preferencesService;
|
||||
_sentMailReceiptService = sentMailReceiptService;
|
||||
}
|
||||
|
||||
public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions)
|
||||
@@ -363,6 +366,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
// 5. Assign all properties synchronously from the pre-loaded in-memory caches - no DB calls here.
|
||||
AssignPropertiesFromCaches(mails, folderCache, accountCache, contactCache);
|
||||
mails.RemoveAll(m => m.AssignedAccount == null || m.AssignedFolder == null);
|
||||
await _sentMailReceiptService.PopulateReceiptStatesAsync(mails).ConfigureAwait(false);
|
||||
|
||||
if (!options.CreateThreads || mails.Count == 0)
|
||||
return [.. mails];
|
||||
@@ -418,6 +422,8 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
mails.AddRange(threadMails.Where(m => m.AssignedAccount != null && m.AssignedFolder != null));
|
||||
}
|
||||
|
||||
await _sentMailReceiptService.PopulateReceiptStatesAsync(mails).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return [.. mails];
|
||||
}
|
||||
@@ -568,6 +574,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
mailCopy.AssignedAccount = account;
|
||||
mailCopy.AssignedFolder = folder;
|
||||
mailCopy.SenderContact = await GetSenderContactForAccountAsync(account, mailCopy.FromAddress).ConfigureAwait(false);
|
||||
await _sentMailReceiptService.PopulateReceiptStateAsync(mailCopy).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<MailCopy> GetSingleMailItemWithoutFolderAssignmentAsync(string mailCopyId)
|
||||
@@ -843,6 +850,8 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
var insertMailTask = InsertMailAsync(mailCopy);
|
||||
|
||||
await Task.WhenAll(mimeSaveTask, insertMailTask).ConfigureAwait(false);
|
||||
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task CreateMailAsyncEx(Guid accountId, NewMailItemPackage package)
|
||||
@@ -919,6 +928,8 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
mailCopy.UniqueId = existingCopyItem.UniqueId;
|
||||
|
||||
await UpdateMailAsync(mailCopy).ConfigureAwait(false);
|
||||
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -933,6 +944,8 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
}
|
||||
|
||||
await InsertMailAsync(mailCopy).ConfigureAwait(false);
|
||||
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Services;
|
||||
|
||||
public class SentMailReceiptService(
|
||||
IDatabaseService databaseService,
|
||||
IFolderService folderService,
|
||||
IAccountService accountService) : BaseDatabaseService(databaseService), ISentMailReceiptService
|
||||
{
|
||||
public async Task PopulateReceiptStateAsync(MailCopy mailCopy)
|
||||
{
|
||||
if (mailCopy == null)
|
||||
return;
|
||||
|
||||
var state = await Connection.FindAsync<SentMailReceiptState>(mailCopy.UniqueId).ConfigureAwait(false);
|
||||
ApplyState(mailCopy, state);
|
||||
}
|
||||
|
||||
public async Task PopulateReceiptStatesAsync(IReadOnlyCollection<MailCopy> mailCopies)
|
||||
{
|
||||
if (mailCopies == null || mailCopies.Count == 0)
|
||||
return;
|
||||
|
||||
var uniqueIds = mailCopies
|
||||
.Select(m => m.UniqueId)
|
||||
.Where(id => id != Guid.Empty)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (uniqueIds.Count == 0)
|
||||
return;
|
||||
|
||||
var placeholders = string.Join(",", uniqueIds.Select(_ => "?"));
|
||||
var states = await Connection.QueryAsync<SentMailReceiptState>(
|
||||
$"SELECT * FROM {nameof(SentMailReceiptState)} WHERE {nameof(SentMailReceiptState.MailUniqueId)} IN ({placeholders})",
|
||||
uniqueIds.Cast<object>().ToArray()).ConfigureAwait(false);
|
||||
|
||||
var stateLookup = states.ToDictionary(s => s.MailUniqueId);
|
||||
|
||||
foreach (var mailCopy in mailCopies)
|
||||
{
|
||||
stateLookup.TryGetValue(mailCopy.UniqueId, out var state);
|
||||
ApplyState(mailCopy, state);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TrackSentMailAsync(MailCopy mailCopy, MimeKit.MimeMessage mimeMessage = null)
|
||||
{
|
||||
if (mailCopy?.AssignedFolder == null || mailCopy.AssignedAccount == null)
|
||||
return;
|
||||
|
||||
if (mailCopy.AssignedFolder.SpecialFolderType != SpecialFolderType.Sent)
|
||||
return;
|
||||
|
||||
var isRequested = mailCopy.IsReadReceiptRequested || mimeMessage.HasReadReceiptRequest();
|
||||
if (!isRequested || string.IsNullOrWhiteSpace(mailCopy.MessageId))
|
||||
return;
|
||||
|
||||
var existing = await Connection.FindAsync<SentMailReceiptState>(mailCopy.UniqueId).ConfigureAwait(false);
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
existing = new SentMailReceiptState
|
||||
{
|
||||
MailUniqueId = mailCopy.UniqueId,
|
||||
AccountId = mailCopy.AssignedAccount.Id,
|
||||
MessageId = MailHeaderExtensions.NormalizeMessageId(mailCopy.MessageId),
|
||||
IsReceiptRequested = true,
|
||||
RequestedAtUtc = mailCopy.CreationDate == default ? DateTime.UtcNow : mailCopy.CreationDate,
|
||||
Status = SentMailReceiptStatus.Requested
|
||||
};
|
||||
|
||||
await Connection.InsertAsync(existing, typeof(SentMailReceiptState)).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.AccountId = mailCopy.AssignedAccount.Id;
|
||||
existing.MessageId = MailHeaderExtensions.NormalizeMessageId(mailCopy.MessageId);
|
||||
existing.IsReceiptRequested = true;
|
||||
if (existing.Status == SentMailReceiptStatus.None)
|
||||
existing.Status = SentMailReceiptStatus.Requested;
|
||||
|
||||
await Connection.UpdateAsync(existing, typeof(SentMailReceiptState)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
ApplyState(mailCopy, existing);
|
||||
ReportUIChange(new MailUpdatedMessage(mailCopy, EntityUpdateSource.Server, MailCopyChangeFlags.ReadReceiptState));
|
||||
}
|
||||
|
||||
public async Task ProcessIncomingReceiptAsync(MailCopy receiptMail, MimeKit.MimeMessage mimeMessage)
|
||||
{
|
||||
if (receiptMail?.AssignedAccount == null || mimeMessage == null)
|
||||
return;
|
||||
|
||||
var parsedReceipt = mimeMessage.ParseReadReceipt();
|
||||
if (!parsedReceipt.IsReadReceipt || string.IsNullOrWhiteSpace(parsedReceipt.OriginalMessageId))
|
||||
return;
|
||||
|
||||
var targetMail = await Connection.FindWithQueryAsync<MailCopy>(
|
||||
"SELECT MailCopy.* FROM MailCopy " +
|
||||
"INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id " +
|
||||
"WHERE MailItemFolder.MailAccountId = ? AND MailCopy.MessageId = ? AND MailItemFolder.SpecialFolderType = ? " +
|
||||
"ORDER BY MailCopy.CreationDate DESC LIMIT 1",
|
||||
receiptMail.AssignedAccount.Id,
|
||||
parsedReceipt.OriginalMessageId,
|
||||
SpecialFolderType.Sent).ConfigureAwait(false);
|
||||
|
||||
if (targetMail == null)
|
||||
return;
|
||||
|
||||
var state = await Connection.FindAsync<SentMailReceiptState>(targetMail.UniqueId).ConfigureAwait(false)
|
||||
?? new SentMailReceiptState
|
||||
{
|
||||
MailUniqueId = targetMail.UniqueId,
|
||||
AccountId = receiptMail.AssignedAccount.Id,
|
||||
MessageId = parsedReceipt.OriginalMessageId,
|
||||
RequestedAtUtc = targetMail.CreationDate == default ? DateTime.UtcNow : targetMail.CreationDate,
|
||||
IsReceiptRequested = true,
|
||||
Status = SentMailReceiptStatus.Requested
|
||||
};
|
||||
|
||||
state.AccountId = receiptMail.AssignedAccount.Id;
|
||||
state.MessageId = parsedReceipt.OriginalMessageId;
|
||||
state.IsReceiptRequested = true;
|
||||
state.Status = SentMailReceiptStatus.Acknowledged;
|
||||
state.AcknowledgedAtUtc = parsedReceipt.AcknowledgedAtUtc ?? DateTime.UtcNow;
|
||||
state.ReceiptMessageUniqueId = receiptMail.UniqueId;
|
||||
|
||||
if (await Connection.FindAsync<SentMailReceiptState>(state.MailUniqueId).ConfigureAwait(false) == null)
|
||||
await Connection.InsertAsync(state, typeof(SentMailReceiptState)).ConfigureAwait(false);
|
||||
else
|
||||
await Connection.UpdateAsync(state, typeof(SentMailReceiptState)).ConfigureAwait(false);
|
||||
|
||||
var folder = await folderService.GetFolderAsync(targetMail.FolderId).ConfigureAwait(false);
|
||||
if (folder == null)
|
||||
return;
|
||||
|
||||
var account = await accountService.GetAccountAsync(folder.MailAccountId).ConfigureAwait(false);
|
||||
targetMail.AssignedFolder = folder;
|
||||
targetMail.AssignedAccount = account;
|
||||
ApplyState(targetMail, state);
|
||||
|
||||
ReportUIChange(new MailUpdatedMessage(targetMail, EntityUpdateSource.Server, MailCopyChangeFlags.ReadReceiptState));
|
||||
}
|
||||
|
||||
private static void ApplyState(MailCopy mailCopy, SentMailReceiptState state)
|
||||
{
|
||||
mailCopy.IsReadReceiptRequested = state?.IsReceiptRequested ?? false;
|
||||
mailCopy.ReadReceiptStatus = state?.Status ?? SentMailReceiptStatus.None;
|
||||
mailCopy.ReadReceiptAcknowledgedAtUtc = state?.AcknowledgedAtUtc;
|
||||
mailCopy.ReadReceiptMessageUniqueId = state?.ReceiptMessageUniqueId;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public static class ServicesContainerSetup
|
||||
|
||||
services.AddTransient<ICalendarService, CalendarService>();
|
||||
services.AddTransient<IMailService, MailService>();
|
||||
services.AddTransient<ISentMailReceiptService, SentMailReceiptService>();
|
||||
services.AddTransient<IFolderService, FolderService>();
|
||||
services.AddTransient<IAccountService, AccountService>();
|
||||
services.AddTransient<IContactService, ContactService>();
|
||||
|
||||
Reference in New Issue
Block a user