Search “C# receive email”, or “csharp receive email” and once the search box eats the hash, the results mostly teach you how to send the email, not receive. The rest assume you already own a mailbox somewhere and want to log into it.
Sure, both are real problems; but I dealt with neither of them.
I explore the use case where your application is the email destination. And I show you how the parsed email payload can become an AI agent’s input to handle the reply-forward cycle.
This guide builds that path end-to-end in ASP.NET Core, then covers the mailbox path (MailKit over IMAP or POP3, Microsoft Graph, Gmail) properly but briefly, because it is well covered elsewhere and you do not need a fifth version of it.
If you need the other direction, the sibling guide on sending email in C# covers SmtpClient, MailKit and the Mailtrap SDK.
Everything here was verified against a live Mailtrap Inbound inbox in September 2026. And, where our own documentation and the wire disagree, I have said so.
How to receive email in C#
The short version:
- Problem 1. You own a mailbox and want to read it, which is a client problem. Most “C# read email” questions are this one. You connect over IMAP, POP3, Microsoft Graph or the Gmail API, authenticate as the mailbox owner, and pull messages down.
- Problem 2. Your application is the destination, which is an infrastructure problem. Mail is addressed to you, something accepts it over SMTP (the Simple Mail Transfer Protocol), parses it, and hands you structured data. That is inbound email, and it’s a different shape of problem: you programmatically receive email without owning a mailbox at all.
To stress, almost every confused thread about receiving email in C# is these two above being treated equally. Someone asks how to receive mail in their ASP.NET C# web app and gets told to run an SMTP server, which is a bit like answering “how do I take payments” with “first, charter a bank.”
One more source of confusion is System.Net.Mail. Its SmtpClient, with its EnableSsl flag and NetworkCredential login, and its MailMessage and MailAddress types only ever sent mail.
There is no receive side in System.Net.Mail, which is why the MailKit vs SmtpClient debate does not apply here: MailKit is the one that can read a mailbox, and Microsoft’s own docs steer you away from SmtpClient for new work anyway.
Check the table below since this is worth dissecting a bit more for clarity.
| Approach | Library or API | Mailbox or domain | Auth | Best for |
|---|---|---|---|---|
| Inbound parse webhook | Mailtrap Inbound Email API | Neither to start; hosted address issued to you | API token | Your app is the destination: ticketing, parsers, agent inboxes |
| Microsoft 365 / Exchange | Microsoft Graph SDK | Existing mailbox | OAuth2 via MSAL, Mail.Read | Reading corporate mailboxes |
| Gmail | MailKit over IMAP, or Gmail API | Existing mailbox | App password (needs 2-Step Verification) or OAuth2 | Reading a Gmail or Workspace mailbox |
| Other IMAP/POP3 hosts | MailKit ImapClient / Pop3Client | Existing mailbox | Password or OAuth2 depending on host | iCloud, Yahoo, self-hosted, generic hosting |
| Your own SMTP listener | SmtpServer (cosullivan) | Domain plus MX plus port 25 | Yours to build | Almost nobody, honestly |
Prerequisites
- .NET 8 or later, and Visual Studio, Rider or the dotnet CLI
- An ASP.NET Core project
- A Mailtrap account with an account-level API token, created under Settings → API Tokens. If you are used to the term API key, it is the same thing.
- Somewhere the internet can reach your endpoint
Technical Note: For local work, any tunnelling tool will do.
Receive inbound email in C# with Mailtrap
Create an inbound email address
Inbound Email comes with Email API/SMTP, so if you have the latter you already have this. The resources themselves nest as folder, then inbox, then messages, with threads grouping a conversation.
The fastest route to the address is Mailtrap CLI:
brew install mailtrap/cli/mailtrap
export MAILTRAP_API_TOKEN=[YOUR_MAILTRAP_API_TOKEN]
mailtrap inbound folders create --name "Support" -o json
mailtrap inbound inboxes create --folder-id [YOUR_FOLDER_ID] --name "Support inbox" -o json
Pass -o json every time. The CLI defaults to a human-readable table, which is pleasant to look at and miserable to parse.
And there are three things worth knowing before you hit a wall with them:
- As hinted, creating folders and inboxes needs an account-level token. A token scoped to a single sending domain can read inbound perfectly well and then returns 403 the moment you try to create anything. If your create call fails with 403 and your read calls work, the API key’s scope is the reason.
- The hosted domain is inbound-mailtrap.io, with a hyphen. You will see the dotted form written in places; it is a marketing simplification and it will not resolve in your code.
- The response carries the generated address. If you would rather receive on your own domain, enable inbound domain receiving under Domain Verification, add the MX record it gives you, and pass domain_id when creating the inbox. That gives you a catch-all at *@your-domain.com.
Verify the webhook signature
This is the part that breaks most implementations, and it breaks them in a way that looks like a Mailtrap problem rather than a code problem.
Here’s the deal – Mailtrap signs each webhook with HMAC-SHA256 and sends the digest in a Mailtrap-Signature header. Note there is no X- prefix; the docs write it lowercase, and since HTTP header names are case-insensitive either spelling works when you read it.
The signature is computed over the raw request body. Not over the object your framework parsed out of the body. Deserializing JSON and re-serializing it reorders keys and normalizes whitespace, and the resulting bytes will not match the digest. Mailtrap’s own .NET example carries a comment saying exactly this.
In ASP.NET Core the trap is specific and easy to fall into. The moment you write a handler that binds the payload as a parameter, model binding consumes the body and you have lost the original bytes. The fix is to take HttpRequest and read the stream yourself.
using System.Text;
using System.Text.Json;
using Mailtrap.Webhooks;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var signingSecret = Environment.GetEnvironmentVariable("MAILTRAP_WEBHOOK_SIGNING_SECRET")
?? "[YOUR_WEBHOOK_SIGNING_SECRET]";
app.MapPost("/inbound", async (HttpRequest request, IInboundQueue queue) =>
{
string rawBody;
using (var reader = new StreamReader(request.Body, Encoding.UTF8))
{
rawBody = await reader.ReadToEndAsync();
}
var signature = request.Headers["Mailtrap-Signature"].ToString();
if (!WebhookSignature.Verify(rawBody, signature, signingSecret))
{
return Results.Unauthorized();
}
// Hand off and return immediately. Fetching the message is slower than
// the sender is willing to wait.
await queue.EnqueueAsync(rawBody);
return Results.Ok();
});
app.Run();
WebhookSignature.Verify comes from the Mailtrap NuGet package. If you need to verify by hand without the SDK, read the SDK source first. The encoding of the digest is not stated in the documentation, and guessing between hex and base64 is a poor use of an afternoon.
If middleware or a filter needs to read the body before your handler does, call request.EnableBuffering() first and reset request.Body.Position to zero after each read. Otherwise the second reader finds an empty stream.
Process incoming email in C#
Here is the structural thing you should know about, it’s worth stating plainly because most inbound-parse services work the other way; the webhook body is a notification, not the message.
It carries an events array, and each event tells you the event type (inbound.message_received), an event ID, a Unix timestamp, the inbox ID, the message ID, and the sender name. It does not carry the subject, the bodies, or the attachments. You take the message ID and then fetch the message.
To the above, there’s one caveat – the webhooks documentation describes what each event contains without publishing the exact JSON key names. So, don’t guess, log your first delivery and bind to what actually arrives. Run this line and you never wonder again:
app.Logger.LogInformation("Inbound webhook payload: {Payload}", rawBody);
Then, you need a plain GET to fetch the message:
GET https://mailtrap.io/api/inbound/inboxes/[YOUR_INBOX_ID]/messages/{id}
Authorization: Bearer [YOUR_MAILTRAP_API_TOKEN]
And this is where the field names pop up:
public sealed record InboundMessage
{
[JsonPropertyName("id")] public string Id { get; init; } = "";
[JsonPropertyName("inbox_id")] public int InboxId { get; init; }
[JsonPropertyName("thread_id")] public string? ThreadId { get; init; }
[JsonPropertyName("from")] public string From { get; init; } = "";
[JsonPropertyName("to")] public string[] To { get; init; } = [];
[JsonPropertyName("subject")] public string Subject { get; init; } = "";
// Not "text" and "html". This is the one that costs people an hour.
[JsonPropertyName("text_body")] public string? TextBody { get; init; }
[JsonPropertyName("html_body")] public string? HtmlBody { get; init; }
// Nullable on purpose. See below.
[JsonPropertyName("attachments")] public InboundAttachment[]? Attachments { get; init; }
}
To read the email body in C# you bind two fields, and they are text_body and html_body. If you write [JsonPropertyName("text")], System.Text.Json binds null without complaint, and you get a bug that survives code review because everyone reads text and thinks yes, that’s the text.
to is an array because a message can carry multiple recipients. The full message also exposes cc, bcc and the raw headers as fields, so getting email headers in C# is a property read here, not a MIME parse.
attachments is nullable for a reason: when a message has no attachments the key is absent from the response rather than present and empty. Bind it to a non-nullable array and your first plain-text email throws a null reference on .Length.
Every message also carries a thread_id, which is how you group a conversation without doing your own header archaeology on References and In-Reply-To. There are dedicated thread endpoints if you want the whole conversation at once, with cursor pagination through last_id.
This record is the input to whatever comes next, whether you save the email to a database, turn the email into a ticket, or hand it to an agent.
Download email attachments in C#
Each attachment arrives as metadata plus a signed download link, already decoded out of its MIME part, here’s an example:
public sealed record InboundAttachment
{
[JsonPropertyName("attachment_id")] public string Id { get; init; } = "";
[JsonPropertyName("filename")] public string Filename { get; init; } = "";
[JsonPropertyName("content_type")] public string ContentType { get; init; } = "";
[JsonPropertyName("size")] public long Size { get; init; }
[JsonPropertyName("download_url")] public string? DownloadUrl { get; init; }
[JsonPropertyName("download_url_expires_at")] public DateTimeOffset? DownloadUrlExpiresAt { get; init; }
}
download_url is a pre-signed S3 link with a one-hour lifetime; the URL itself carries X-Amz-Expires=3600, and download_url_expires_at tells you when it dies. Therefore, download it during processing. If you store that URL in your database and fetch it tomorrow, you get a 403 and a confusing incident.
There is no separate attachments endpoint. Both of the paths you would guess at return 404. The attachment metadata on the message is the whole interface.
Inbound email limits and failure modes
These four cost me time during implementation. None of them is in the docs, the CLI help, or any SDK readme. However, at the time of publication, I’m actively working with Mailtrap product team to fix and document these.
- A message size depends on your plan. On lower plans – Free and Basic, it’s at ~7 MB, and on Business and Enterprise plans it’s 30 MB. Note that if the message is too big it’s rejected at SMTP with 552 5.3.4 Message exceeds max size of xy bytes. Nothing is stored and no webhook fires. From inside your application this is completely silent: the inbox just sits there, serene and empty, while someone insists they sent it twice. The cap applies to the encoded MIME, and base64 inflates binary by roughly a third.
domain_idcomes back populated on hosted inboxes as well as custom-domain ones. That matters because setting a custom FROM address on a reply only works on custom-domain inboxes. The obvious check quietly gives you the wrong answer.- The CLI returns a reduced view of every message. On version 0.6.0,
'messages get'returns 10 of the API’s 22 fields and'messages list'returns 8 of 18 per row. Among the silently dropped ones are attachments, cc, bcc, headers, and the threading fields. A message with an attachment shows no attachments key at all, so anything reading CLI output concludes there are none. This is reported and a fix is in progress. - Pagination is unusable from the CLI for the same reason. The API returns an envelope of data,
total_countandlast_id; the CLI returns the bare array and discards the envelope, which leaves –last-id as an input flag whose value you can never obtain from CLI output. Use the API or an SDK for anything that pages.
With all that said, the CLI is still the right tool for provisioning, replying, forwarding and deleting. And it’s extremely token efficient, should you wire it to an AI workflow. Just don’t build a parser on it.
Build an AI agent email inbox in C#
Here’s the fun part, since by now, you should have resolved the quirks and have the correct data to hand off to your agent to close the loop.
Once mail arrives as structured JSON, an agent’s input problem is solved. The inbox becomes an address you can give out, and replies go back through the same API, so a support agent or an internal bot can send and receive emails from a real address without anyone running a mail server.
Replying is one call, and reply_all and forward sit beside it:
POST /api/inbound/inboxes/[YOUR_INBOX_ID]/messages/{id}/reply
Threads matter more here than anywhere else. Because every message carries thread_id, an agent can pull a whole conversation in order and reason over it, rather than reconstructing context from headers. The thread endpoints return messages oldest first, which is the order a model wants to read them in.
Mailtrap’s Agent Inbox is built on exactly this flow, and the Inbound Email API underneath it is the same surface this guide has been using. There is also a Mailtrap Skill for receiving inbound email, it’s currently in closed beta undergoing testing. But once merged to existing Mailtrap Skills, it will let an agent provision and read its own inbox without you writing the plumbing first.
Two honest limits, because you will hit them if you build something ambitious.
- There are no WebSocket events, so real-time means your webhook, not a socket.
- There is no built-in draft review step, so if you want a human to approve an agent’s reply before it sends, that queue is yours to build.
Test email receiving in C#
This is the part I would not skip, and it is the one thing here that no competing service lets you do in one place. More importantly, testing the loop significantly reduces the chances for your agent to send spam, even if there’s no human in the loop.
Mailtrap’s Email Sandbox captures outbound mail so it never reaches a real person. Inbound Email receives. Put them together, and you can send and receive emails inside one integration test: send a message, wait for it to arrive, and assert on what your handler did with it.
Here’s a simple example:
[Fact]
public async Task InboundMessage_CreatesTicket()
{
await _sender.SendAsync(to: _inboxAddress, subject: "Printer on fire");
var message = await WaitForMessageAsync(subject: "Printer on fire",
timeout: TimeSpan.FromSeconds(30));
var ticket = await _tickets.FindByMessageIdAsync(message.Id);
Assert.NotNull(ticket);
Assert.Equal("Printer on fire", ticket!.Subject);
}
In addition, I suggest you poll with a timeout. Waiting for an email in a test is a polling problem: inbound delivery is fast but not instant, and a hard-coded Task.Delay is how a green test suite becomes a flaky one at the worst possible moment.
Important Note: I need to be precise about one thing, because the names may invite confusion: Email Sandbox and Inbound Email are different products with different addresses. Sandbox addresses live on inbox.mailtrap.io and exist to catch mail your application sends. Inbound addresses live on inbound-mailtrap.io and exist to receive mail from the outside world. They are not interchangeable but can work great together to create an entire email workflow!
Read email from an existing mailbox
If your problem really is “connect to a mailbox and read the inbox,” here is the working minimum for each path: reading email from IMAP or POP3 with MailKit, from Microsoft 365 through Graph, and from Gmail. These are well documented elsewhere, and most .NET applications that read a mailbox use one of them, so I will not pad them out.
MailKit over IMAP
MailKit is the de facto C# IMAP library. It is on NuGet as the MailKit package. MimeKit comes with it and does the parsing, and the shape has been stable for years:
using var client = new ImapClient();
client.Connect("imap.example.com", 993, true);
client.Authenticate("[YOUR_MAILBOX_USER]", "[YOUR_MAILBOX_PASSWORD]");
var inbox = client.Inbox;
inbox.Open(FolderAccess.ReadOnly);
for (int i = 0; i < inbox.Count; i++)
{
var message = inbox.GetMessage(i);
Console.WriteLine(message.Subject);
}
client.Disconnect(true);
You get a MimeMessage back, which is MimeKit’s message type and where the parsing lives: From, To, Subject, TextBody, HtmlBody, and an Attachments collection you can iterate. It is not System.Net.Mail’s MailMessage; MimeKit can create a MimeMessage from a MailMessage for legacy code, but not the other way round. The same object loads a saved .eml file, so testing against fixtures is easy.
Authenticate takes a username and password or a NetworkCredential, and every call has an async twin such as ConnectAsync, which is what you want inside a background service.
MailKit supports IMAP IDLE through IdleAsync, so the server can notify you of new mail without a polling timer. If your host does not support IDLE, poll on an interval and fetch unread emails with a search for NotSeen.
For POP3 there is Pop3Client with a similar shape, though POP3 downloads and typically deletes, so reach for IMAP unless you specifically want that. If you are on OpenPop.NET’s POPClient from an old tutorial, it has not seen a release in over a decade; Pop3Client is the replacement. If you are weighing the two protocols, we compared POP3 vs. IMAP separately.
Microsoft 365 through Graph
Basic authentication is gone from Exchange Online and is not coming back.
So IMAP against a Microsoft 365 mailbox means OAuth2 or nothing. But MailKit’s SaslMechanismOAuth2 with an Entra token works if you must stay on IMAP.
For new work, use Microsoft Graph: register an app in Entra and request Mail.Read, acquire a token with MSAL, and read through GraphServiceClient. Delegated permissions act as a signed-in user; application permissions are what you want for a daemon reading a shared mailbox.
BTW, it is as complicated as it sounds. 😀
Anyway, if you are still on EWS, this is genuinely urgent. Microsoft begins blocking EWS requests from non-Microsoft applications to Exchange Online on 1 October 2026, with full removal on 1 April 2027.
The one-time exemption required registering your client ID by the end of August 2026, and that window has already closed. EWS in on-premises Exchange Server is unaffected. Graph does not yet have complete parity, so check archive mailbox access and public folder operations before you commit to a date.
Gmail
Connect to imap.gmail.com on 993 with MailKit, the receiving-side counterpart of the Gmail SMTP setup. POP3 is pop.gmail.com on 995, if you have a reason to prefer it.
For a personal script, an app password works, provided the account has 2-Step Verification enabled.
For anything multi-user, or any Workspace account, app passwords are not an option and you need OAuth2 through SaslMechanismOAuth2, which is XOAUTH2 on the wire.
MailKit ships a dedicated guide for the Google flow, and Google’s own documentation now steers firmly toward OAuth.
Additional findings
Lo and behold 😀 the username-and-password snippet you found on Stack Overflow probably does not work any more. But that’s not all.
If you haven’t already guessed it, basic authentication is disabled in every Exchange Online tenant, and Microsoft is explicit that nobody can turn it back on, including Microsoft support.
Google’s app passwords still exist but require 2-Step Verification. Interestingly, they are unavailable on work and school accounts entirely, and Google’s own help page now describes them as “not recommended and unnecessary in most cases.”
Wrapping up
The decision underneath all of this is short.
- If you own the mailbox, you are writing a client, and MailKit or Graph is your answer: ImapClient, Pop3Client or GraphServiceClient, not anything in System.Net.Mail.
- If your application is the destination, you want an inbound webhook, and the work is signature verification, a fetch, and a handler that returns fast. (use Mailtrap 😉)
And I’d like to remind you of some specifics to save your time when your application is the destination:
- Read the raw body before anything parses it
- Expect a notification rather than a message
- Bind text_body and html_body rather than text and html
- Treat attachments as nullable
- Download attachment URLs inside the hour they live
That is what it takes to receive email in C# when nobody is going to open a mailbox. The reference implementation this is based on builds clean, and the facts above came out of running it rather than reading about it.
FAQ
Is SmtpClient obsolete, and what should I use to receive email in C# instead?
SmtpClient only ever sent mail; it never received any, so it was never the tool for this. Microsoft’s docs recommend against SmtpClient for new development, and MailKit is the usual replacement on the send side too. To read a mailbox you own, use MailKit’s ImapClient or Pop3Client. If your application is the destination for the mail, use an inbound parse webhook instead.
Can I receive emails in C# without running a mail server?
Yes. An inbound parse webhook gives your application a receiving address without a mail server, MX records, or a mailbox to log into. Mailtrap accepts the message over SMTP, stores it, and calls your endpoint with the message ID; you fetch the parsed message over HTTPS.
What happens to an inbound message that is too large?
With Mailtrap, a message whose encoded size exceeds your plan’s cap is rejected at SMTP with 552 5.3.4 Message exceeds max size. Nothing is stored, and no webhook fires, so the failure is invisible from inside your application. The cap is roughly 7 MB on Free and Basic and 30 MB on Business and Enterprise, and because it applies after encoding, raw attachments should stay well under it.
Why does my inbound webhook signature never validate?
Almost always because something parsed the body before you verified it. The HMAC is computed over the raw bytes, and deserializing then re-serializing JSON reorders keys and changes whitespace. In ASP.NET Core, take HttpRequest and read the stream yourself rather than binding the payload as a parameter.
Do I need a domain to receive email in an application?
No. A hosted address on inbound-mailtrap.io is issued through the API with no DNS work at all. If you would rather receive on your own domain, enable inbound domain receiving, add the MX record, and pass domain_id when creating the inbox for a catch-all at *@your-domain.com.
How do I read email from Microsoft 365 now that basic auth is gone?
Use Microsoft Graph with an app registration, OAuth2 through MSAL, and the Mail.Read scope. Basic authentication is disabled in all tenants and cannot be re-enabled by anyone. If you are on EWS, note that blocking starts on 1 October 2026 and full removal follows on 1 April 2027.
IMAP or POP3?
IMAP, in almost every case. It leaves messages on the server and syncs folders and flags across clients. POP3 downloads and usually deletes, which is only what you want when the mailbox is a transfer buffer and not a record. If you came to C# to receive email hoping for a single answer, that is it: IMAP for a mailbox you own and an inbound webhook for mail addressed to your application.