Files
AI-Studio/app/Tests/Tools/Mail/MailSummaryReaderTests.cs
T
Thorsten Sommer c4400c0ff6
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Added mailboxes as local data sources (#1022)
2026-10-04 12:26:26 +02:00

185 lines
8.3 KiB
C#

using AIStudio.Tools.Databases.IndexStore;
using AIStudio.Tools.Mail;
using MailKit;
using MimeKit;
namespace AIStudio.Tests.Tools.Mail;
/// <summary>
/// Checks what AI Studio keeps of the summary a server reports about a mail.
/// </summary>
/// <remarks>
/// The summaries are built by hand, with the header blocks of the fixtures: the sync fetches the
/// whole header block with every mail and reads everything from there.
/// </remarks>
[TestFixture]
public sealed class MailSummaryReaderTests
{
/// <summary>
/// Where a part sits in the mail, e.g. "1.2". Nothing here looks at it.
/// </summary>
private const string PART_SPECIFIER = "1";
[Test]
public void TheIdentityComesFromTheHeaderBlock()
{
var summary = Summary("header-injection.eml");
summary.EmailId = "M6d99ac3275bb4e";
summary.Size = 2048;
var identity = MailSummaryReader.ReadIdentity(summary);
Assert.Multiple(() =>
{
Assert.That(identity.EmailId, Is.EqualTo("M6d99ac3275bb4e"));
Assert.That(identity.GmailMessageId, Is.Null);
Assert.That(identity.MessageId, Is.EqualTo("<header-injection@example.org>"));
Assert.That(identity.Date, Is.EqualTo(new DateTimeOffset(2026, 9, 30, 13, 0, 0, TimeSpan.FromHours(2))));
Assert.That(identity.FromAddresses, Is.EqualTo(new[] { "mueller@example.org" }));
Assert.That(identity.Subject, Does.StartWith("Hello"));
Assert.That(identity.Size, Is.EqualTo(2048));
});
}
[Test]
public void EveryAddressKeepsItsHeader()
{
var addresses = MailSummaryReader.ReadAddresses(MailFixtures.Load("header-injection.eml").Headers);
Assert.Multiple(() =>
{
Assert.That(addresses.Count(address => address.Role is MailAddressRole.TO), Is.EqualTo(12), "An address of the second To line is lost.");
Assert.That(addresses[0], Is.EqualTo(new MailAddressRecord(MailAddressRole.FROM, "mueller@example.org", "Müller, Jürgen")));
Assert.That(addresses[^1], Is.EqualTo(new MailAddressRecord(MailAddressRole.CC, "mueller@example.org", "mueller@example.org")));
Assert.That(addresses.Any(address => address.Role is MailAddressRole.SENDER or MailAddressRole.REPLY_TO), Is.False, "A header the mail does not have was made up.");
});
}
[Test]
public void SenderAndReplyToCountOnlyWhenTheMailSetsThem()
{
var headers = new HeaderList();
headers.Add("From", "Alice <alice@example.org>");
headers.Add("Sender", "Assistant <assistant@example.org>");
headers.Add("Reply-To", "Team <team@example.org>");
Assert.That(MailSummaryReader.ReadAddresses(headers), Is.EqualTo(new[]
{
new MailAddressRecord(MailAddressRole.FROM, "alice@example.org", "Alice"),
new MailAddressRecord(MailAddressRole.SENDER, "assistant@example.org", "Assistant"),
new MailAddressRecord(MailAddressRole.REPLY_TO, "team@example.org", "Team"),
}));
}
[Test]
public void TheTextSourceTakesAttachmentsAndEncryptionFromTheStructure()
{
var attachment = new BodyPartBasic(new ContentType("application", "pdf"), PART_SPECIFIER)
{
ContentDisposition = new ContentDisposition(ContentDisposition.Attachment) { FileName = "board-report.pdf" },
};
var readable = Summary("priority-with-attachment.eml");
readable.Body = Multipart("mixed", new BodyPartText(new ContentType("text", "plain"), PART_SPECIFIER), attachment);
var encrypted = Summary("smime-enveloped.eml");
encrypted.Body = new BodyPartBasic(new ContentType("application", "pkcs7-mime") { Parameters = { { "smime-type", "enveloped-data" } } }, PART_SPECIFIER);
var readableSource = MailSummaryReader.ReadTextSource(readable, new MailTextParts(null, "Please review the attached report."));
var encryptedSource = MailSummaryReader.ReadTextSource(encrypted, null);
Assert.Multiple(() =>
{
Assert.That(readableSource.AttachmentNames, Is.EqualTo(new[] { "board-report.pdf" }));
Assert.That(readableSource.StructureEncryption, Is.EqualTo(MailEncryptionKind.NONE));
Assert.That(readableSource.TextBody, Is.EqualTo("Please review the attached report."));
Assert.That(readableSource.HtmlBody, Is.Null);
Assert.That(encryptedSource.StructureEncryption, Is.EqualTo(MailEncryptionKind.SMIME));
Assert.That(MailTextBuilder.Build(encryptedSource).Body, Is.Empty);
});
}
[Test]
public void TheSignatureOfASignedMailIsNoAttachment()
{
var signature = new BodyPartBasic(new ContentType("application", "pkcs7-signature"), PART_SPECIFIER)
{
ContentDisposition = new ContentDisposition(ContentDisposition.Attachment) { FileName = "smime.p7s" },
};
var signed = Summary("smime-clear-signed.eml");
signed.Body = Multipart("signed", new BodyPartText(new ContentType("text", "plain"), PART_SPECIFIER), signature);
Assert.Multiple(() =>
{
Assert.That(MailSummaryReader.ReadAttachments(signed), Is.Empty);
Assert.That(MailSummaryReader.ReadTextSource(signed, new MailTextParts(null, "Signed, but readable for everybody.")).AttachmentNames, Is.Empty);
});
}
[Test]
public void TheFlagsAreRead()
{
Assert.Multiple(() =>
{
Assert.That(MailSummaryReader.ReadFlags(MessageFlags.Seen | MessageFlags.Flagged | MessageFlags.Recent), Is.EqualTo(new MailFlags(IsSeen: true, IsFlagged: true, IsAnswered: false)));
Assert.That(MailSummaryReader.ReadFlags(MessageFlags.Answered), Is.EqualTo(new MailFlags(IsSeen: false, IsFlagged: false, IsAnswered: true)));
Assert.That(MailSummaryReader.ReadFlags(null), Is.EqualTo(new MailFlags(IsSeen: false, IsFlagged: false, IsAnswered: false)));
});
}
[Test]
public void TheHeaderBlockStaysAsTheServerSentIt() =>
Assert.That(MailSummaryReader.ReadHeaderBlock(MailFixtures.Load("header-injection.eml").Headers), Does.Contain("Subject: =?utf-8?b?SGVsbG8NCkZyb206IGNlb0BleGFtcGxlLm9yZw==?="), "The trust check needs the encoded words as they were sent.");
[Test]
public void TheMailHashFollowsTheMail()
{
var summary = Summary("priority-with-attachment.eml");
summary.Size = 2048;
var hash = MailSummaryReader.ComputeMailHash(summary);
var sameMail = Summary("priority-with-attachment.eml");
sameMail.Size = 2048;
var otherSize = Summary("priority-with-attachment.eml");
otherSize.Size = 2049;
Assert.Multiple(() =>
{
Assert.That(hash, Does.Match("^[0-9a-f]{64}$"));
Assert.That(MailSummaryReader.ComputeMailHash(sameMail), Is.EqualTo(hash));
Assert.That(MailSummaryReader.ComputeMailHash(otherSize), Is.Not.EqualTo(hash));
Assert.That(MailSummaryReader.ComputeMailHash(Summary("smime-enveloped.eml")), Is.Not.EqualTo(hash));
});
}
[Test]
public void MessageIdsAreReadWithoutTheirBrackets()
{
var headers = new HeaderList();
headers.Add("In-Reply-To", "<second@example.org>");
headers.Add("References", "<first@example.org>\r\n <second@example.org>");
Assert.Multiple(() =>
{
Assert.That(MailHeaders.ReadMessageIds(headers, HeaderId.References), Is.EqualTo(new[] { "first@example.org", "second@example.org" }));
Assert.That(MailHeaders.ReadMessageIds(headers, HeaderId.InReplyTo), Is.EqualTo(new[] { "second@example.org" }));
Assert.That(MailHeaders.ReadMessageIds(headers, HeaderId.MessageId), Is.Empty);
});
}
[Test]
public void AMailFetchedWithoutItsHeaderBlockIsRefused() =>
Assert.That(() => MailSummaryReader.ReadIdentity(new MessageSummary(0)), Throws.InvalidOperationException);
private static MessageSummary Summary(string fixtureName) => new(0) { Headers = MailFixtures.Load(fixtureName).Headers };
private static BodyPartMultipart Multipart(string mediaSubtype, params BodyPart[] parts)
{
var multipart = new BodyPartMultipart(new ContentType("multipart", mediaSubtype), PART_SPECIFIER);
foreach (var part in parts)
multipart.BodyParts.Add(part);
return multipart;
}
}