---
title: "Read email over IMAP in C#"
url: https://recursiveai.org/onemail/cookbook/read-email-over-imap/
description: "Read email over IMAP in C# with OneMail.Client: connect over TLS, select INBOX read-only and fetch subjects. Includes a disposable local test mailbox."
version: "1.0.1"
---

# Read email over IMAP in C#

**OneMail** is a free, open-source .NET email library from Recursive AI for email files, IMAP, POP3 and SMTP. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneMail.Client) · [GitHub](https://github.com/recursive-ai-oss/onemail-csharp) · [Docs](https://recursiveai.org/onemail/docs/) · [Download example (.zip)](https://recursiveai.org/onemail/cookbook/read-email-over-imap/read-email-over-imap.zip)

Read INBOX subjects over TLS. Native platforms only, not browsers.

```shell
dotnet add package RecursiveAI.OneMail.Client
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneMail.Client.Imap;
using static System.Environment;

string Env(string name) =>
    GetEnvironmentVariable(name)!;
// Local test port: 1993; real TLS servers normally use 993.
await using var client =
    await ImapClient.ConnectAsync(
        Env("MAIL_HOST"), 1993, new() {
            Connection = new() {
                ImplicitTls = true } });
await client.LoginAsync(Env("MAIL_USER"),
    Env("MAIL_PASSWORD"));
await client.SelectAsync("INBOX",
    new() { ReadOnly = true });
await foreach (var m in client.FetchAsync(
    ImapSet.All, new() { Envelope = true }))
    Console.WriteLine(m.Envelope!.Subject);
await client.LogoutAsync();
```

```text
Local test message
```

For the local test mailbox, first run `source ./setup-test.sh` in Bash (Linux, Docker and OpenSSL required).

Unzip [the example](https://recursiveai.org/onemail/cookbook/read-email-over-imap/read-email-over-imap.zip), then `dotnet run`.

## The full program

The test setup supplies the three `MAIL_*` environment variables used by the snippet at the top.

The download includes `setup-test.sh`, a Linux/Bash fixture requiring Docker and OpenSSL. From the unzipped directory, run:

```shell
source ./setup-test.sh
dotnet run
```

The script starts a disposable Dovecot server on the IPv6 loopback interface. It sets `MAIL_HOST`, `MAIL_USER` and `MAIL_PASSWORD` to test-only values, and `SSL_CERT_FILE` to a freshly generated certificate. Certificate validation stays enabled; the fixture’s certificate file is supplied through `SSL_CERT_FILE` without changing the machine’s trust store. No real account is contacted. Ports 1993 and 1995 are the local test ports, not the usual public-service ports.

The mailbox contains this entire synthetic message (CRLF line endings):

```text
From: sender@example.test
To: reader@example.test
Subject: Local test message
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8

Hello from the test mailbox.
```

The snippet at the top prints subjects. The full program also prints the selected mailbox’s message count, read-only state and each message’s UID (its identifier within that mailbox).

```csharp
using RecursiveAI.OneMail.Client.Imap;

string Env(string name) => Environment.GetEnvironmentVariable(name)
    ?? throw new InvalidOperationException($"Set {name} before running.");
// Local test port: 1993; real TLS servers normally use 993.
await using var client = await ImapClient.ConnectAsync(
    Env("MAIL_HOST"), 1993,
    new() { Connection = new() { ImplicitTls = true } });
await client.LoginAsync(Env("MAIL_USER"), Env("MAIL_PASSWORD"));
var mailbox = await client.SelectAsync("INBOX", new() { ReadOnly = true });
Console.WriteLine($"Messages: {mailbox.Exists}; read-only: {mailbox.ReadOnly}");
await foreach (var message in client.FetchAsync(ImapSet.All,
    new() { Uid = true, Envelope = true }))
    Console.WriteLine($"UID {message.Uid}: {message.Envelope!.Subject}");
await client.LogoutAsync();
```

```text
Messages: 1; read-only: True
UID 1: Local test message
```

A server `NO` or `BAD` completion throws `ImapCommandException`; connection, TLS or login failures stop the program before it can read the mailbox.

## Variations

### Download a complete message as EML

In the full program, replace the fetch loop with this code. `client` is the authenticated client with INBOX selected; UID 1 comes from the fresh test mailbox’s output, not from a message’s position.

```csharp
// UID 1 is printed by the full program for the fresh test mailbox.
using (var file = File.Create("message.eml"))
    await client.DownloadAsync(1, "", file);
Console.WriteLine("Saved message.eml");
```

The replacement prints `Saved message.eml` after the mailbox summary. It uses `BODY.PEEK`, so it does not mark the message as seen.

Stop the disposable server when finished:

```shell
docker stop "$MAIL_TEST_CONTAINER"
```

## Related recipes

- [Parse a local EML file](https://recursiveai.org/onemail/cookbook/parse-eml/)
- [Download email over POP3](https://recursiveai.org/onemail/cookbook/download-email-over-pop3/)
- [Send an email](https://recursiveai.org/onemail/cookbook/send-email/)

## The short answer

To read email over IMAP in C#, install `RecursiveAI.OneMail.Client`, connect with `ImapClient.ConnectAsync`, authenticate, select a mailbox and enumerate `FetchAsync`. Request `Envelope` when you need subjects and addresses rather than the complete MIME message. The example selects INBOX read-only and does not delete messages.

IMAP access needs a running mail server. A local EML parser does not connect to one. The included fixture gives you a message and a TLS endpoint without entering an account password. Once that works, change the host and port for your own server and provide credentials through the environment rather than embedding them in source.

## How do I check email using IMAP?

Connect to the server’s IMAP endpoint, not its SMTP endpoint. This example uses implicit TLS: encryption starts as soon as the TCP connection opens. The fixture maps that service to port 1993; a public implicit-TLS IMAP endpoint normally uses 993, as described in OneMail’s [connection options](https://github.com/recursive-ai-oss/onemail-csharp/blob/fe2e40ab5837ca7e9e9641c1dcdfccf209094f89/src/OneMail.Client/Protocol/ProtocolConnectionOptions.cs). Use the hostname and port supplied by your mail administrator.

The fixture’s self-signed certificate is trusted through `SSL_CERT_FILE` only for the test shell. For a real service, leave the default platform certificate validation in place and use its trusted hostname. Do not copy a certificate-accept-all callback into an application to make a failed handshake disappear.

The password-login sample requires a server that permits that authentication method. OneMail also exposes SASL authentication, but this example neither obtains access tokens nor configures a hosted mail account. A provider that requires [another login flow, such as Gmail or Outlook.com](https://recursiveai.org/onemail/docs/providers/), needs that flow configured before the fetch can succeed.

Selecting a mailbox establishes the context for subsequent fetches. `ReadOnly = true` requests EXAMINE instead of a writable SELECT. An empty INBOX yields no subjects; it is not an authentication failure. The full program prints the count so the distinction is visible. See the [IMAP session documentation](https://recursiveai.org/onemail/docs/imap/) for the selected-mailbox model and other commands.

## What are the disadvantages of using IMAP?

For an importer, the practical concern is that the mailbox can change while you read it. Message sequence numbers are positions, not durable identifiers: an expunge renumbers messages above the removed one. The full program therefore requests UIDs alongside envelopes. When keeping a cache across sessions, track the mailbox’s `UidValidity` as well as UIDs. If `UidValidity` changes, the old cache is no longer valid for that mailbox.

Fetching envelopes does not download bodies or attachments. Use the EML variation when you need the original complete message. `DownloadAsync` takes a UID and streams a section to a destination; an empty section selects the complete message. A buffered source fetch instead has a configured buffered-literal limit, so streaming is the appropriate path when you do not want the whole source buffered in memory.

OneMail permits only one command at a time on this client. Finish a fetch enumeration before sending another command, rather than starting a download inside the active fetch loop. A concurrent command throws `InvalidOperationException`. If cancellation interrupts an in-flight command, dispose the client rather than reusing a connection left partway through a response.
