MailSlurp logo

guides

C# email tutorial for SMTP sending and inbox testing

Send and receive email in C# with MailKit and MailSlurp. Practical .NET patterns for SMTP setup, inbox waits, attachments, and verification flows.

View MarkdownAgent setup
C# email tutorial for SMTP sending and inbox testing article preview

Email code tends to fail in two directions: the application does not send, or nobody checks what actually arrived. A useful C# test covers both halves without borrowing a teammate's inbox or hoping a five-second sleep is long enough.

For a modern .NET project, use MailKit to submit email over SMTP and the MailSlurp C# SDK to create a private test address, wait for the message, and inspect its content. That gives your test a real inbox and a reliable finish line.

Quick answer: how do I send and receive email in C#?

Use this combination:

  • MailKit builds MIME messages and sends them through SMTP.
  • MailSlurp creates email addresses your C# tests can control through an API.
  • WaitForControllerApi waits for the expected message instead of making the test guess when delivery has finished.
  • NUnit, xUnit, or MSTest checks the subject, sender, body, links, codes, and attachments.

If your application already sends with another provider, keep that production path. Give the application a MailSlurp address as the recipient, then use the MailSlurp SDK to read and test the delivered email. You only need the MailKit SMTP section when your C# code itself must submit a message.

Microsoft still supports System.Net.Mail.SmtpClient for existing applications, but its .NET documentation recommends MailKit or another modern library for new development. This guide therefore uses MailKit as the main path.

Install the C# email packages

Add the MailSlurp NuGet package and MailKit to your project:

dotnet add package mailslurp
dotnet add package MailKit

Restore packages if your tooling does not do it automatically:

dotnet restore

Pin dependency versions through your normal project or central package management setup. That keeps a CI run next month from quietly compiling against a different client than the one you tested today.

Keep the MailSlurp API key out of source control

Create an API key in your MailSlurp dashboard and pass it to the test process as an environment variable. Do not paste a working key into a test fixture, screenshot, or example commit.

The SDK uses the x-api-key header. This setup also gives long-running inbox waits enough HTTP time to finish:

using mailslurp.Client;

var apiKey = Environment.GetEnvironmentVariable("MAILSLURP_API_KEY")
    ?? throw new InvalidOperationException(
        "Set MAILSLURP_API_KEY before running the email tests.");

var config = new Configuration();
config.ApiKey.Add("x-api-key", apiKey);
config.Timeout = 130_000;

The API client's timeout should be a little longer than the inbox wait you request later. Otherwise the local HTTP client can give up just before the email arrives, which is a particularly unhelpful kind of suspense.

Create a private inbox for the test

The following tested example creates an SMTP_INBOX and fetches its SMTP access details. That inbox type is useful here because the next section submits mail through SMTP.

using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
using mailslurp.Api;
using mailslurp.Client;
using mailslurp.Model;
var config = new Configuration();
config.ApiKey.Add("x-api-key", YOUR_API_KEY);
var inboxController = new InboxControllerApi(config);
var inbox = await inboxController.CreateInboxWithOptionsAsync(new CreateInboxDto
{
    InboxType = CreateInboxDto.InboxTypeEnum.SMTPINBOX,
    Name = "My test inbox",
});
var accessDetails = await inboxController.GetImapSmtpAccessAsync(inbox.Id);

When you only need to receive an email from the application under test, a regular private MailSlurp inbox is enough. Use one inbox per test run, worker, or customer journey so an old verification email cannot satisfy a new assertion.

For a deeper look at inbox lifecycle choices, see test email accounts for QA and CI and the Email Sandbox.

Send an email with MailKit

Build the message with MimeKit. The sender must match the inbox or sending identity allowed by your SMTP credentials, while the recipient should be the address that owns the workflow you are testing.

var message = new MimeMessage();
message.From.Add(new MailboxAddress(name: inbox.Name, address: inbox.EmailAddress));
message.To.Add(new MailboxAddress(name: inbox.Name, address: inbox.EmailAddress));
message.Subject = "Test Email";
message.Body = new TextPart("plain")
{
    Text = @"Hello World!"
};

Now connect with the host, port, username, and password returned for the inbox:

using (var client = new SmtpClient())
{
    await client.ConnectAsync(host: accessDetails.SecureSmtpServerHost , port: accessDetails.SecureSmtpServerPort, SecureSocketOptions.StartTls);
    // Remove other authentication mechanisms except for PLAIN
    client.AuthenticationMechanisms.Remove("XOAUTH2");
    client.AuthenticationMechanisms.Remove("CRAM-MD5");
    client.AuthenticationMechanisms.Remove("LOGIN");
    await client.AuthenticateAsync(userName: accessDetails.SecureSmtpUsername, password: accessDetails.SecureSmtpPassword);
    await client.SendAsync(message);
    await client.DisconnectAsync(true);
}

SecureSocketOptions.StartTls asks the server to upgrade the SMTP connection before authentication. MailKit documents that StartTls fails if the server does not advertise STARTTLS, which is preferable to silently sending credentials over an unencrypted connection.

Notice that the client disconnects cleanly even after the message has been submitted. In production code, also pass a CancellationToken from the request or background job so shutdowns and timeouts do not leave work hanging.

Receive email in C# without polling or fixed sleeps

SMTP delivery is asynchronous. Task.Delay(5000) does not prove a message will arrive within five seconds; it only guarantees that the test spends five seconds waiting before it starts guessing.

Use MailSlurp's wait endpoint instead:

var waitForControllerApi = new WaitForControllerApi(config);
var email = await waitForControllerApi.WaitForLatestEmailAsync(inbox.Id, 120000, true);

The arguments in this example mean:

  • wait on this inbox ID
  • allow up to 120,000 milliseconds for delivery
  • return an unread message

A wait should have a generous upper bound and still fail decisively. A 60- to 120-second limit is sensible for most end-to-end email tests because it accommodates real delivery without letting a broken build linger forever.

Once you have the email ID, fetch the full message when the assertion needs fields such as attachments:

var emailController = new EmailControllerApi(_configuration);
var fullEmail = emailController.GetEmail(email.Id);
Assert.That(fullEmail.Attachments, Has.Count.EqualTo(1));

Assert the message a customer would receive

An arrival-only assertion is a smoke test, not a customer journey. Check the parts that can actually break the experience:

  • the expected sender and recipient
  • a stable subject fragment
  • required plain-text or HTML content
  • verification, reset, unsubscribe, and deep links
  • OTP or magic-link expiry behavior
  • attachment names, types, and contents
  • headers needed for routing or authentication checks

This NUnit-shaped example keeps the wait asynchronous and checks several useful fields together:

var waitApi = new WaitForControllerApi(config);
var email = await waitApi.WaitForLatestEmailAsync(
    inboxId: inbox.Id,
    timeout: 120_000,
    unreadOnly: true);

Assert.Multiple(() =>
{
    Assert.That(email.Subject, Does.Contain("Confirm your email"));
    Assert.That(email.To, Does.Contain(inbox.EmailAddress));
    Assert.That(email.Body, Does.Contain("Activate account"));
});

Prefer an isolated inbox over a more elaborate subject search when tests run in parallel. Isolation is easy to understand when a build fails at 2 a.m., and it prevents inbox archaeology from becoming part of the debugging process.

Extract a verification code safely

Match the label as well as the digits. A broad \d{6} pattern can accidentally capture an order number, date, or support reference from the same message.

using System.Text.RegularExpressions;

var body = email.Body
    ?? throw new InvalidOperationException("The verification email had no body.");

var match = Regex.Match(
    body,
    @"verification code(?: is|:)\s*([0-9]{6})",
    RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

Assert.That(match.Success, Is.True, "No six-digit verification code was found.");
var verificationCode = match.Groups[1].Value;

Use that code in the real UI or API flow and assert the resulting account state. The test is complete when the customer can continue, not when the inbox happens to contain six promising digits.

For browser-driven journeys, connect the inbox step to Selenium with C# or the Playwright email testing guide.

Test attachments and HTML email

MailKit's BodyBuilder creates multipart messages without hand-writing MIME boundaries:

var message = new MimeMessage();
message.From.Add(new MailboxAddress("Acme Billing", inbox.EmailAddress));
message.To.Add(MailboxAddress.Parse(inbox.EmailAddress));
message.Subject = "Your monthly report";

var body = new BodyBuilder
{
    TextBody = "Your report is attached.",
    HtmlBody = "<p>Your report is <strong>attached</strong>.</p>"
};
body.Attachments.Add("fixtures/monthly-report.pdf");
message.Body = body.ToMessageBody();

After delivery, assert that the attachment exists, then download it by email and attachment ID when you need to check the bytes. Also inspect both HTML and plain-text bodies. A beautiful HTML invoice is not much comfort if its text alternative is empty.

Use email client testing when layout matters across supported client and device previews, and use email integration testing to make the content checks part of CI.

Validate an address before sending

Inbox testing proves what happened after a message was sent. Address verification answers a different question: whether a proposed recipient has valid syntax and useful domain or mailbox signals before you send.

var verificationControllerApi = new EmailVerificationControllerApi(config);
var results =await verificationControllerApi.ValidateEmailAddressListAsync(new ValidateEmailAddressListOptions(["contact@mailslurp.dev"]) );

Use email address verification for signup forms, imports, or sending workflows that need this preflight. Keep it separate from the end-to-end inbox assertion so a validation result is never mistaken for proof that a message arrived.

A reliable C# verification-email test

For signup, password reset, or account-change email, use this sequence:

  1. Create a fresh MailSlurp inbox for the test.
  2. Enter that address in the real application flow.
  3. Trigger the email and wait for it through WaitForControllerApi.
  4. Assert sender, recipient, subject, and customer-facing copy.
  5. Extract the expected link or code from the received body.
  6. Complete the action in the browser or API.
  7. Assert the account is verified, recovered, or updated.
  8. Expire or delete test-owned resources according to your suite's cleanup policy.

This catches failures that a mocked mail service cannot see: bad templates, wrong environment links, delayed delivery, missing personalization, and a verification endpoint that no longer accepts the token it created.

Troubleshooting C# email tests

Authentication fails before send

Fetch fresh SMTP access details for the inbox and confirm that the host, port, username, and password all come from the same response. Do not mix dashboard values from one inbox with credentials from another.

The SMTP connection cannot start TLS

Use the secure host and port returned by MailSlurp with SecureSocketOptions.StartTls. Do not bypass certificate validation to make a test green. A TLS failure is evidence worth keeping.

The wait times out

Confirm the application used the generated inbox address, then inspect the sending provider's response and the MailSlurp inbox. Keep the SDK HTTP timeout longer than the wait-endpoint timeout. Avoid immediately adding retries until you know whether the message was never sent, rejected, delayed, or sent to the wrong address.

The test reads yesterday's message

Create a new inbox per run or use a strict message-matching strategy. unreadOnly: true helps, but isolation is the clearest defence against stale mail and parallel-test collisions.

The body assertion fails on HTML

Inspect the exact received body rather than the template before rendering. Email platforms can transform HTML, rewrite links, and add tracking markup. Assert stable customer-visible content and parsed URLs instead of brittle whitespace or an entire HTML snapshot.

FAQ

Can C# receive email without IMAP?

Yes. A MailSlurp inbox receives the message, and your C# test reads it through the REST SDK. That is usually simpler for automated tests than maintaining a long-lived IMAP connection and mailbox credentials.

Should I use System.Net.Mail.SmtpClient or MailKit?

Use MailKit for new .NET development. Keep System.Net.Mail.SmtpClient only where an existing application already depends on it and a migration is not yet practical.

How long should a C# test wait for email?

Give real delivery enough room, usually 60 to 120 seconds, and keep the SDK's HTTP timeout slightly longer. The wait should be bounded so a missing message produces a useful failure instead of a stuck build.

Can I test OTP and password-reset emails with the same approach?

Yes. Create an isolated inbox, trigger the real flow, wait for the message, extract the labelled code or link, complete the action, and assert the final application state.

Can MailSlurp send email from C# too?

Yes. You can send through MailSlurp's SMTP access with MailKit or use the C# SDK's sending endpoints. Choose the path that matches the application behavior you want to test.

Next resources

The practical pattern is pleasantly small: send through the path your application really uses, receive in a private inbox, and finish the customer action. Once that loop is in CI, email stops being a hopeful side effect and becomes something your C# test can prove.