Fixing issues with replies.
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
using FluentAssertions;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
using Wino.Core.Domain.Misc;
|
||||
using Xunit;
|
||||
|
||||
namespace Wino.Core.Tests.Services;
|
||||
|
||||
public class MailHeaderExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void MessageIdGenerator_Generate_ReturnsGuidAtWinoMailDomain()
|
||||
{
|
||||
var generated = MessageIdGenerator.Generate();
|
||||
|
||||
generated.Should().MatchRegex("^<[0-9a-fA-F-]{36}@wino-mail\\.app>$");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildReferencesChain_DeduplicatesAndAppendsParentMessageId()
|
||||
{
|
||||
var chain = MailHeaderExtensions.BuildReferencesChain(
|
||||
["<root@domain.com>", "middle@domain.com", "<middle@domain.com>"],
|
||||
"<parent@domain.com>");
|
||||
|
||||
chain.Should().Equal("root@domain.com", "middle@domain.com", "parent@domain.com");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
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<ISignatureService>();
|
||||
var authProvider = new Mock<IAuthenticationProvider>();
|
||||
var mimeFileService = new Mock<IMimeFileService>();
|
||||
mimeFileService
|
||||
.Setup(x => x.SaveMimeMessageAsync(It.IsAny<Guid>(), It.IsAny<MimeMessage>(), It.IsAny<Guid>()))
|
||||
.ReturnsAsync(true);
|
||||
mimeFileService
|
||||
.Setup(x => x.CreateHTMLPreviewVisitor(It.IsAny<MimeMessage>(), It.IsAny<string>()))
|
||||
.Returns<MimeMessage, string>((_, _) => new HtmlPreviewVisitor(string.Empty));
|
||||
|
||||
var preferencesService = new Mock<IPreferencesService>();
|
||||
preferencesService.SetupProperty(x => x.ComposerFont, "Calibri");
|
||||
preferencesService.SetupProperty(x => x.ComposerFontSize, 12);
|
||||
|
||||
var contactPictureFileService = new Mock<IContactPictureFileService>();
|
||||
|
||||
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);
|
||||
|
||||
return new MailService(
|
||||
db,
|
||||
folderService,
|
||||
contactService,
|
||||
accountService,
|
||||
signatureService.Object,
|
||||
mimeFileService.Object,
|
||||
preferencesService.Object);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user