Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions Sources/Mailozaurr.Tests/ImapMessageReaderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MailKit;
using MailKit.Net.Imap;
using MimeKit;
using Moq;
using Xunit;

namespace Mailozaurr.Tests;

public sealed class ImapMessageReaderTests
{
[Fact]
public async Task ReadAsync_UsesResolvedFolderAndTruncatesBodies()
{
var uid = new UniqueId(42);
var message = new MimeMessage();
message.From.Add(MailboxAddress.Parse("sender@example.com"));
message.To.Add(MailboxAddress.Parse("recipient@example.com"));
message.Subject = "subject";
message.Date = new DateTimeOffset(2026, 3, 17, 10, 0, 0, TimeSpan.Zero);
var builder = new BodyBuilder {
TextBody = "1234567890",
HtmlBody = "<p>abcdefghij</p>"
};
builder.Attachments.Add("report.txt", new byte[] { 1, 2, 3 });
message.Body = builder.ToMessageBody();

var folder = new Mock<IMailFolder>();
folder.SetupGet(f => f.FullName).Returns("Inbox/Sub");
folder.SetupGet(f => f.IsOpen).Returns(true);
folder.SetupGet(f => f.Access).Returns(FolderAccess.ReadOnly);
folder.Setup(f => f.GetMessageAsync(uid, It.IsAny<CancellationToken>(), null))
.ReturnsAsync(message);

var personalRoot = new Mock<IMailFolder>();
personalRoot.Setup(f => f.GetSubfolder("Sub", It.IsAny<CancellationToken>()))
.Returns(folder.Object);

var client = new Mock<ImapClient> { CallBase = true };
client.Setup(c => c.Inbox).Returns(Mock.Of<IMailFolder>(f => f.FullName == "Inbox"));
client.Setup(c => c.GetFolder("Sub", It.IsAny<CancellationToken>()))
.Throws(new FolderNotFoundException("Sub"));
client.Setup(c => c.GetFolder(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns((string name, CancellationToken _) => name.Length == 0 ? personalRoot.Object : throw new FolderNotFoundException(name));

var personalNamespaces = new FolderNamespaceCollection();
personalNamespaces.Add(new FolderNamespace('.', ""));
client.Setup(c => c.GetFolder(It.IsAny<FolderNamespace>()))
.Returns(personalRoot.Object);
client.Setup(c => c.PersonalNamespaces)
.Returns(personalNamespaces);

var result = await ImapMessageReader.ReadAsync(
client.Object,
new ImapMessageReadRequest(uid, "Sub", 8),
CancellationToken.None);

Assert.Equal(42L, result.Uid);
Assert.Equal("Inbox/Sub", result.Folder);
Assert.Equal("subject", result.Subject);
Assert.True(result.TextTruncated);
Assert.True(result.HtmlTruncated);
Assert.True(result.HasAttachments);
var attachment = Assert.Single(result.Attachments);
Assert.Equal("report.txt", attachment.FileName);
}

[Fact]
public async Task ReadAsync_UsesInboxWhenFolderOmitted()
{
var uid = new UniqueId(7);
var message = new MimeMessage();
message.Body = new TextPart("plain") { Text = "body" };

var inbox = new Mock<IMailFolder>();
inbox.SetupGet(f => f.FullName).Returns("Inbox");
inbox.SetupGet(f => f.IsOpen).Returns(true);
inbox.SetupGet(f => f.Access).Returns(FolderAccess.ReadOnly);
inbox.Setup(f => f.GetMessageAsync(uid, It.IsAny<CancellationToken>(), null))
.ReturnsAsync(message);

var client = new Mock<ImapClient> { CallBase = true };
client.Setup(c => c.Inbox).Returns(inbox.Object);
client.Setup(c => c.PersonalNamespaces).Returns(new FolderNamespaceCollection());

var result = await ImapMessageReader.ReadAsync(
client.Object,
new ImapMessageReadRequest(uid, null, 128),
CancellationToken.None);

Assert.Equal("Inbox", result.Folder);
Assert.False(result.TextTruncated);
Assert.False(result.HasAttachments);
}
}
57 changes: 57 additions & 0 deletions Sources/Mailozaurr.Tests/ImapSessionServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Threading.Tasks;
using MailKit.Net.Imap;
using MailKit.Security;
using Xunit;

namespace Mailozaurr.Tests;

public class ImapSessionServiceTests {
private sealed class FakeImapClient : ImapClient {
public bool AuthenticateCalled { get; set; }
public new bool Authenticated { get; set; }
private int _timeout;

public override bool IsAuthenticated => Authenticated;

public override int Timeout {
get => _timeout;
set => _timeout = value;
}

public override Task ConnectAsync(string host, int port, SecureSocketOptions options, System.Threading.CancellationToken cancellationToken = default) {
return Task.CompletedTask;
}
}

[Fact]
public async Task ConnectAsync_UsesConnectionAndAuthenticationSettings() {
var fakeClient = new FakeImapClient();
var previousFactory = ImapConnector.ClientFactory;
try {
ImapConnector.ClientFactory = () => fakeClient;
var request = new ImapSessionRequest {
Connection = new ImapConnectionRequest(
"imap.example.test",
1993,
SecureSocketOptions.SslOnConnect,
timeout: 4321,
retryCount: 0),
UserName = "user@example.test",
Secret = "secret",
AuthenticateAsync = (client, _) => {
((FakeImapClient)client).AuthenticateCalled = true;
((FakeImapClient)client).Authenticated = true;
return Task.CompletedTask;
}
};

var client = await ImapSessionService.ConnectAsync(request);

Assert.Same(fakeClient, client);
Assert.True(fakeClient.AuthenticateCalled);
Assert.Equal(4321, fakeClient.Timeout);
} finally {
ImapConnector.ClientFactory = previousFactory;
}
}
}
69 changes: 69 additions & 0 deletions Sources/Mailozaurr.Tests/SmtpSessionServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Threading.Tasks;
using MailKit.Security;
using Xunit;

namespace Mailozaurr.Tests;

public class SmtpSessionServiceTests {
[Fact]
public async Task ConnectAndAuthenticateAsync_ReturnsSuccess() {
var request = new SmtpSessionRequest {
Server = "smtp.test",
Port = 587,
SecureSocketOptions = SecureSocketOptions.Auto,
UserName = "user",
Password = "pass",
ConnectAsync = _ => Task.FromResult(new SmtpResult(true, EmailAction.Connect, string.Empty, string.Empty, "smtp.test", 587, TimeSpan.Zero)),
AuthenticateAsync = _ => Task.FromResult(new SmtpResult(true, EmailAction.Authenticate, string.Empty, string.Empty, "smtp.test", 587, TimeSpan.Zero))
};

var smtp = new Smtp();
var result = await SmtpSessionService.ConnectAndAuthenticateAsync(smtp, request);

Assert.True(result.IsSuccess);
Assert.Equal(SecureSocketOptions.Auto, result.SecureSocketOptions);
Assert.Null(result.ErrorCode);
}

[Fact]
public async Task ConnectAndAuthenticateAsync_FlagsConnectFailures() {
var request = new SmtpSessionRequest {
Server = "smtp.test",
Port = 587,
SecureSocketOptions = SecureSocketOptions.Auto,
UserName = "user",
Password = "pass",
ConnectAsync = _ => Task.FromResult(new SmtpResult(false, EmailAction.Connect, string.Empty, string.Empty, "smtp.test", 587, TimeSpan.Zero, error: "nope"))
};

var smtp = new Smtp();
var result = await SmtpSessionService.ConnectAndAuthenticateAsync(smtp, request);

Assert.False(result.IsSuccess);
Assert.Equal("connect_failed", result.ErrorCode);
Assert.Equal("nope", result.Error);
Assert.True(result.IsTransient);
}

[Fact]
public async Task ConnectAndAuthenticateAsync_FlagsAuthFailures() {
var request = new SmtpSessionRequest {
Server = "smtp.test",
Port = 587,
SecureSocketOptions = SecureSocketOptions.Auto,
UserName = "user",
Password = "pass",
ConnectAsync = _ => Task.FromResult(new SmtpResult(true, EmailAction.Connect, string.Empty, string.Empty, "smtp.test", 587, TimeSpan.Zero)),
AuthenticateAsync = _ => Task.FromResult(new SmtpResult(false, EmailAction.Authenticate, string.Empty, string.Empty, "smtp.test", 587, TimeSpan.Zero, error: "bad auth"))
};

var smtp = new Smtp();
var result = await SmtpSessionService.ConnectAndAuthenticateAsync(smtp, request);

Assert.False(result.IsSuccess);
Assert.Equal("auth_failed", result.ErrorCode);
Assert.Equal("bad auth", result.Error);
Assert.False(result.IsTransient);
}
}
60 changes: 60 additions & 0 deletions Sources/Mailozaurr/Connections/ImapSessionService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using MailKit.Net.Imap;

namespace Mailozaurr;

/// <summary>
/// Describes the parameters required for an authenticated IMAP session.
/// </summary>
public sealed class ImapSessionRequest {
/// <summary>
/// Gets or sets the underlying connection request.
/// </summary>
public ImapConnectionRequest Connection { get; init; } = new("localhost", 993);

/// <summary>
/// Gets or sets the auth username.
/// </summary>
public string UserName { get; init; } = string.Empty;

/// <summary>
/// Gets or sets the auth secret or token.
/// </summary>
public string Secret { get; init; } = string.Empty;

/// <summary>
/// Gets or sets the protocol auth mode.
/// </summary>
public ProtocolAuthMode AuthMode { get; init; } = ProtocolAuthMode.Basic;

/// <summary>
/// Gets or sets an optional authenticate delegate used for testing/custom flows.
/// </summary>
public Func<ImapClient, CancellationToken, Task>? AuthenticateAsync { get; init; }
}

/// <summary>
/// Helpers for establishing authenticated IMAP sessions.
/// </summary>
public static class ImapSessionService {
/// <summary>
/// Connects and authenticates an IMAP client from a reusable session request.
/// </summary>
public static Task<ImapClient> ConnectAsync(ImapSessionRequest request, CancellationToken cancellationToken = default) {
if (request is null) {
throw new ArgumentNullException(nameof(request));
}

return ImapConnector.ConnectAsync(
request.Connection,
request.AuthenticateAsync ?? ((client, ct) => ProtocolAuth.AuthenticateImapAsync(
client,
request.UserName,
request.Secret,
request.AuthMode,
ct)),
cancellationToken);
}
}
Loading
Loading