using System.Net.Sockets;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Mail;
using MailKit;
using MailKit.Net.Imap;
using MailKit.Security;
namespace AIStudio.Tests.Tools.Mail;
///
/// Checks the parts of the IMAP connection which decide something without a server.
///
///
/// Which failure an exception stands for matters more than it seems: only a rejected sign-in may
/// pause the mailbox, and a sign-in counted as a network failure would be tried again every
/// synchronization, until the directory locks the account.
///
[TestFixture]
public sealed class ImapMailboxConnectorTests
{
private const string PASSWORD = "correct horse battery staple";
private static readonly DataSourceMailbox MAILBOX = new()
{
Host = "imap.example.org",
Port = 993,
TransportSecurity = MailboxTransportSecurity.SSL_ON_CONNECT,
AuthMethod = MailboxAuthMethod.PASSWORD,
Username = "someone@example.org",
};
private static IEnumerable ExceptionsWithTheirFailure()
{
yield return Case(new AuthenticationException("The server rejected the credentials."), MailboxConnectionFailure.AUTHENTICATION_FAILED);
yield return Case(new SslHandshakeException("The certificate is not trusted."), MailboxConnectionFailure.TLS_FAILED);
yield return Case(new SocketException((int)SocketError.HostNotFound), MailboxConnectionFailure.NETWORK_UNAVAILABLE);
yield return Case(new IOException("The connection was reset.", new SocketException((int)SocketError.ConnectionReset)), MailboxConnectionFailure.NETWORK_UNAVAILABLE);
yield return Case(new TimeoutException(), MailboxConnectionFailure.NETWORK_UNAVAILABLE);
yield return Case(new OperationCanceledException(), MailboxConnectionFailure.NETWORK_UNAVAILABLE);
yield return Case(new ImapProtocolException("The server sent garbage."), MailboxConnectionFailure.SERVER_ERROR);
yield return Case(new FolderNotFoundException("Archive"), MailboxConnectionFailure.SERVER_ERROR);
yield break;
static TestCaseData Case(Exception exception, MailboxConnectionFailure failure) => new TestCaseData(exception, failure).SetArgDisplayNames(exception.GetType().Name, failure.ToString());
}
private static IEnumerable InvalidSettings()
{
yield return Case("unknown transport security", MAILBOX with { TransportSecurity = MailboxTransportSecurity.UNKNOWN }, PASSWORD);
yield return Case("unknown auth method", MAILBOX with { AuthMethod = MailboxAuthMethod.UNKNOWN }, PASSWORD);
yield return Case("no host", MAILBOX with { Host = " " }, PASSWORD);
yield return Case("a URL as host", MAILBOX with { Host = "imaps://imap.example.org" }, PASSWORD);
yield return Case("port 0", MAILBOX with { Port = 0 }, PASSWORD);
yield return Case("port 65536", MAILBOX with { Port = 65536 }, PASSWORD);
yield return Case("no username", MAILBOX with { Username = string.Empty }, PASSWORD);
yield return Case("no password", MAILBOX, string.Empty);
yield break;
static TestCaseData Case(string name, DataSourceMailbox mailbox, string password) => new TestCaseData(mailbox, password).SetArgDisplayNames(name);
}
[TestCaseSource(nameof(ExceptionsWithTheirFailure))]
public void AnExceptionOfTheClientStandsForItsFailure(Exception exception, MailboxConnectionFailure failure)
{
Assert.That(ImapMailboxConnector.Classify(exception, CancellationToken.None), Is.EqualTo(failure));
}
[Test]
public void ACancellationByTheUserIsNoFailure()
{
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
Assert.That(ImapMailboxConnector.Classify(new OperationCanceledException(cancellation.Token), cancellation.Token), Is.Null);
}
[Test]
public void AnExceptionOfAnotherKindIsLeftAlone()
{
Assert.That(ImapMailboxConnector.Classify(new InvalidOperationException(), CancellationToken.None), Is.Null);
}
[TestCaseSource(nameof(InvalidSettings))]
public async Task InvalidSettingsNeverReachTheServer(DataSourceMailbox mailbox, string password)
{
//
// The host of the mailbox does not exist. Were anything sent, the failure would be a network
// failure instead of this one.
//
await using var connector = new ImapMailboxConnector();
var exception = Assert.ThrowsAsync(() => connector.ConnectAsync(mailbox, password, MailServerPolicy.ANY_SERVER, CancellationToken.None));
Assert.That(exception?.Failure, Is.EqualTo(MailboxConnectionFailure.INVALID_SETTINGS));
}
[Test]
public async Task AServerTheOrganizationDoesNotAllowIsNeverContacted()
{
//
// As above, the host of the mailbox does not exist. Were anything sent, the failure would be
// a network failure instead of this one.
//
await using var connector = new ImapMailboxConnector();
var policy = new MailServerPolicy(true, ["imap.intra.example.org"]);
var exception = Assert.ThrowsAsync(() => connector.ConnectAsync(MAILBOX, PASSWORD, policy, CancellationToken.None));
Assert.That(exception?.Failure, Is.EqualTo(MailboxConnectionFailure.SERVER_NOT_ALLOWED));
}
[TestCase("Projects", '/', ExpectedResult = true)]
[TestCase("Projekte 2026", '.', ExpectedResult = true)]
[TestCase("Ablage/Alt", '.', ExpectedResult = true)]
[TestCase("Ablage/Alt", '/', ExpectedResult = false)]
[TestCase("Ablage.Alt", '.', ExpectedResult = false)]
[TestCase("Flat", '\0', ExpectedResult = true)]
[TestCase("", '/', ExpectedResult = false)]
[TestCase(" ", '/', ExpectedResult = false)]
[TestCase(" Projects", '/', ExpectedResult = false)]
[TestCase("All*", '/', ExpectedResult = false)]
[TestCase("100%", '/', ExpectedResult = false)]
[TestCase("Line\nBreak", '/', ExpectedResult = false)]
public bool AFolderNameNeedsNoReservedCharacter(string name, char directorySeparator) => ImapMailboxConnector.IsValidFolderName(name, directorySeparator);
[Test]
public void AFolderNameHasAMaximumLength()
{
Assert.Multiple(() =>
{
Assert.That(ImapMailboxConnector.IsValidFolderName(new string('a', ImapMailboxConnector.MAX_FOLDER_NAME_LENGTH), '/'), Is.True);
Assert.That(ImapMailboxConnector.IsValidFolderName(new string('a', ImapMailboxConnector.MAX_FOLDER_NAME_LENGTH + 1), '/'), Is.False);
});
}
[TestCase(FolderAttributes.None, ExpectedResult = MailFolderSpecialUse.NONE)]
[TestCase(FolderAttributes.HasChildren | FolderAttributes.Subscribed, ExpectedResult = MailFolderSpecialUse.NONE)]
[TestCase(FolderAttributes.Sent, ExpectedResult = MailFolderSpecialUse.SENT)]
[TestCase(FolderAttributes.Drafts, ExpectedResult = MailFolderSpecialUse.DRAFTS)]
[TestCase(FolderAttributes.Archive, ExpectedResult = MailFolderSpecialUse.ARCHIVE)]
[TestCase(FolderAttributes.All, ExpectedResult = MailFolderSpecialUse.ALL)]
[TestCase(FolderAttributes.Flagged, ExpectedResult = MailFolderSpecialUse.FLAGGED)]
[TestCase(FolderAttributes.Important, ExpectedResult = MailFolderSpecialUse.IMPORTANT)]
[TestCase(FolderAttributes.Trash | FolderAttributes.HasNoChildren, ExpectedResult = MailFolderSpecialUse.TRASH)]
[TestCase(FolderAttributes.Junk | FolderAttributes.Important, ExpectedResult = MailFolderSpecialUse.JUNK)]
public MailFolderSpecialUse AFolderIsForWhatTheServerAnnounces(FolderAttributes attributes) => ImapMailboxConnector.ToSpecialUse(attributes);
}