.NET and xUnit
dotnet add package Mailcatchr.SdkRead the key and mailbox id from the environment or user secrets, never from source.
A shared fixture
Section titled “A shared fixture”public sealed class MailFixture : IDisposable{ public MailcatchrClient Client { get; } = new(new MailcatchrClientOptions { ApiKey = Environment.GetEnvironmentVariable("MAILCATCHR_API_KEY") ?? throw new InvalidOperationException("MAILCATCHR_API_KEY is not set"), });
public string MailboxId { get; } = Environment.GetEnvironmentVariable("MAILCATCHR_MAILBOX_ID")!;
public static string UniqueAddress(string label) => $"{label}-{DateTimeOffset.UtcNow:yyyyMMddHHmmssfff}@acme-ci.mailcatchr.com";
public void Dispose() => Client.Dispose();}The test
Section titled “The test”public sealed class PasswordResetTests(MailFixture mail) : IClassFixture<MailFixture>{ [Fact] public async Task Reset_email_contains_a_working_link() { var address = MailFixture.UniqueAddress("reset"); await App.RequestPasswordResetAsync(address);
var summary = await mail.Client.AwaitMessageAsync(mail.MailboxId, to: address, subject: "Reset your password", timeout: TimeSpan.FromSeconds(30)); summary.Should().NotBeNull("the reset email should arrive within 30 s");
var message = await mail.Client.GetMessageAsync(mail.MailboxId, summary!.Id); var link = Regex.Match(message.TextBody!, @"https://\S+/reset\?token=\S+").Value; link.Should().NotBeEmpty();
var response = await App.HttpClient.GetAsync(link); response.StatusCode.Should().Be(HttpStatusCode.OK); }}AwaitMessageAsync returns null on timeout rather than throwing, so the assertion message can say what was expected.
Reading over IMAP instead
Section titled “Reading over IMAP instead”When the code under test is itself a mail client, read the mailbox with MailKit and per-mailbox credentials. See Mail clients.