using FluentAssertions; using MimeKit; using Moq; 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.Domain.Interfaces; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Tests.Helpers; using Wino.Services; using Xunit; namespace Wino.Core.Tests.Services; public class MailThreadingTests : IAsyncLifetime { private InMemoryDatabaseService _databaseService = null!; private MailService _mailService = null!; private MailAccount _account = null!; private MailItemFolder _draftFolder = null!; public async Task InitializeAsync() { _databaseService = new InMemoryDatabaseService(); await _databaseService.InitializeAsync(); _account = new MailAccount { Id = Guid.NewGuid(), Name = "Threading Test Account", Address = "me@test.local", SenderName = "Test User", ProviderType = MailProviderType.IMAP4 }; _draftFolder = new MailItemFolder { Id = Guid.NewGuid(), MailAccountId = _account.Id, FolderName = "Drafts", SpecialFolderType = SpecialFolderType.Draft, IsSystemFolder = true, IsSynchronizationEnabled = true }; var preferences = new MailAccountPreferences { Id = Guid.NewGuid(), AccountId = _account.Id, IsNotificationsEnabled = true, IsSignatureEnabled = false }; var alias = new MailAccountAlias { Id = Guid.NewGuid(), AccountId = _account.Id, AliasAddress = _account.Address, ReplyToAddress = _account.Address, IsPrimary = true, IsRootAlias = true, IsVerified = true }; await _databaseService.Connection.InsertAsync(_account, typeof(MailAccount)); await _databaseService.Connection.InsertAsync(_draftFolder, typeof(MailItemFolder)); await _databaseService.Connection.InsertAsync(preferences, typeof(MailAccountPreferences)); await _databaseService.Connection.InsertAsync(alias, typeof(MailAccountAlias)); _mailService = BuildMailService(_databaseService); } public async Task DisposeAsync() => await _databaseService.DisposeAsync(); [Fact] public async Task CreateDraftAsync_EmptyDraft_AssignsGeneratedMessageId() { var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync( _account.Id, new DraftCreationOptions { Reason = DraftCreationReason.Empty }); var mimeMessage = draftBase64MimeMessage.GetMimeMessageFromBase64(); draftMailCopy.MessageId.Should().MatchRegex("^[0-9a-fA-F-]{36}@wino-mail\\.app$"); mimeMessage.MessageId.Should().Be(draftMailCopy.MessageId); mimeMessage.Headers[HeaderId.MessageId].Should().Be(MailHeaderExtensions.ToHeaderMessageId(draftMailCopy.MessageId)); } [Fact] public async Task CreateDraftAsync_Reply_SetsInReplyToReferencesAndReplySubject() { const string parentMessageId = "original@domain.com"; var referencedMimeMessage = CreateReferencedMimeMessage("From outlook", parentMessageId); var referencedMailCopy = new MailCopy { UniqueId = Guid.NewGuid(), Id = Guid.NewGuid().ToString(), ThreadId = "provider-thread-id", MessageId = parentMessageId }; var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync( _account.Id, new DraftCreationOptions { Reason = DraftCreationReason.Reply, ReferencedMessage = new ReferencedMessage { MimeMessage = referencedMimeMessage, MailCopy = referencedMailCopy } }); var mimeMessage = draftBase64MimeMessage.GetMimeMessageFromBase64(); draftMailCopy.InReplyTo.Should().Be(parentMessageId); draftMailCopy.References.Should().Be(parentMessageId); draftMailCopy.Subject.Should().Be("Re: From outlook"); draftMailCopy.ThreadId.Should().Be(referencedMailCopy.ThreadId); mimeMessage.InReplyTo.Should().Be(parentMessageId); MailHeaderExtensions.NormalizeReferences(mimeMessage.Headers[HeaderId.References]).Should().Be(parentMessageId); } [Fact] public async Task CreateDraftAsync_Reply_AppendsReferencesChainOnce() { const string rootMessageId = "root@domain.com"; const string middleMessageId = "middle@domain.com"; const string parentMessageId = "parent@domain.com"; var referencedMimeMessage = CreateReferencedMimeMessage("Re: Existing subject", parentMessageId); referencedMimeMessage.References.Add(rootMessageId); referencedMimeMessage.References.Add(middleMessageId); var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync( _account.Id, new DraftCreationOptions { Reason = DraftCreationReason.Reply, ReferencedMessage = new ReferencedMessage { MimeMessage = referencedMimeMessage, MailCopy = new MailCopy { UniqueId = Guid.NewGuid(), Id = Guid.NewGuid().ToString(), MessageId = parentMessageId } } }); var mimeMessage = draftBase64MimeMessage.GetMimeMessageFromBase64(); draftMailCopy.References.Should().Be($"{rootMessageId};{middleMessageId};{parentMessageId}"); draftMailCopy.Subject.Should().Be("Re: Existing subject"); MailHeaderExtensions.NormalizeReferences(mimeMessage.Headers[HeaderId.References]) .Should().Be($"{rootMessageId};{middleMessageId};{parentMessageId}"); } [Fact] public async Task CreateDraftAsync_Reply_FallsBackToReferencedMailCopyThreadingMetadata() { const string rootMessageId = "root@domain.com"; const string parentMessageId = "copy-parent@domain.com"; var referencedMimeMessage = CreateReferencedMimeMessage("Fallback subject"); referencedMimeMessage.Headers.Remove(HeaderId.MessageId); var referencedMailCopy = new MailCopy { UniqueId = Guid.NewGuid(), Id = Guid.NewGuid().ToString(), MessageId = parentMessageId, References = rootMessageId }; var (draftMailCopy, _) = await _mailService.CreateDraftAsync( _account.Id, new DraftCreationOptions { Reason = DraftCreationReason.Reply, ReferencedMessage = new ReferencedMessage { MimeMessage = referencedMimeMessage, MailCopy = referencedMailCopy } }); draftMailCopy.InReplyTo.Should().Be(parentMessageId); draftMailCopy.References.Should().Be($"{rootMessageId};{parentMessageId}"); } private static MimeMessage CreateReferencedMimeMessage(string subject, string? messageId = null) { var message = new MimeMessage(); message.From.Add(new MailboxAddress("Sender", "sender@example.com")); message.To.Add(new MailboxAddress("Recipient", "recipient@example.com")); message.Subject = subject; message.Body = new TextPart("plain") { Text = "Body" }; if (!string.IsNullOrWhiteSpace(messageId)) message.MessageId = messageId; return message; } private static MailService BuildMailService(InMemoryDatabaseService db) { var signatureService = new Mock(); var authProvider = new Mock(); var mimeFileService = new Mock(); mimeFileService .Setup(x => x.SaveMimeMessageAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(true); mimeFileService .Setup(x => x.CreateHTMLPreviewVisitor(It.IsAny(), It.IsAny())) .Returns((_, _) => new HtmlPreviewVisitor(string.Empty)); var preferencesService = new Mock(); preferencesService.SetupProperty(x => x.ComposerFont, "Calibri"); preferencesService.SetupProperty(x => x.ComposerFontSize, 12); var contactPictureFileService = new Mock(); var accountService = new AccountService( db, signatureService.Object, authProvider.Object, mimeFileService.Object, preferencesService.Object, contactPictureFileService.Object); var folderService = new FolderService(db, accountService); var contactService = new ContactService(db); var sentMailReceiptService = new SentMailReceiptService(db, folderService, accountService); return new MailService( db, folderService, contactService, accountService, signatureService.Object, mimeFileService.Object, preferencesService.Object, sentMailReceiptService); } }