---
title: "Download email over POP3 in C#"
url: https://recursiveai.org/onemail/cookbook/download-email-over-pop3/
description: "Download email over POP3 in C# as EML files using OneMail.Client and TLS. Enumerate messages without deleting them, with a disposable local test server."
version: "1.0.1"
---

# Download email over POP3 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/download-email-over-pop3/download-email-over-pop3.zip)

Download EML files without deleting server copies. 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.Pop3;
using static System.Environment;

string Env(string name) =>
    GetEnvironmentVariable(name)!;
// Local test port: 1995; real TLS servers normally use 995.
await using var pop =
    await Pop3Client.ConnectAsync(
        Env("MAIL_HOST"), 1995, new() {
            Connection = new() {
                ImplicitTls = true } });
await pop.LoginAsync(Env("MAIL_USER"),
    Env("MAIL_PASSWORD"));
foreach (var m in await pop.UidlAsync())
{
    using var file = File.Create(
        $"message-{m.Number}.eml");
    await pop.RetrieveAsync(m.Number, file);
    Console.WriteLine(
        $"Saved message-{m.Number}.eml");
}
await pop.QuitAsync();
```

```text
Saved message-1.eml
```

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/download-email-over-pop3/download-email-over-pop3.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 writes one EML file per message, overwriting a same-named file in the current directory. The full program also asks the server how many messages remain after downloading.

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

string Env(string name) => Environment.GetEnvironmentVariable(name)
    ?? throw new InvalidOperationException($"Set {name} before running.");
// Local test port: 1995; real TLS servers normally use 995.
await using var pop = await Pop3Client.ConnectAsync(
    Env("MAIL_HOST"), 1995,
    new() { Connection = new() { ImplicitTls = true } });
await pop.LoginAsync(Env("MAIL_USER"), Env("MAIL_PASSWORD"));
foreach (var message in await pop.UidlAsync())
{
    string path = $"message-{message.Number}.eml";
    using var file = File.Create(path);
    await pop.RetrieveAsync(message.Number, file);
    Console.WriteLine($"Saved {path}");
}
var status = await pop.StatAsync();
Console.WriteLine($"Messages still on server: {status.Count}");
await pop.QuitAsync();
```

```text
Saved message-1.eml
Messages still on server: 1
```

A server `-ERR` response throws `Pop3CommandException`; a failed retrieval may leave a partial local file, so do not record a message as downloaded until retrieval completes.

## Variations

### Inspect the subject without saving a file

Replace the full program’s download loop with this code. `pop` is the connected, authenticated client. `GetMessageAsync` parses the downloaded MIME message; MIME is the message format carrying headers and body parts.

```csharp
foreach (var message in await pop.UidlAsync())
{
    var entity = await pop.GetMessageAsync(message.Number);
    Console.WriteLine(
        entity.Headers.GetFirst("Subject")?.Value);
}
```

```text
Local test message
Messages still on server: 1
```

Stop the disposable server when finished:

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

## Related recipes

- [Read email over IMAP](https://recursiveai.org/onemail/cookbook/read-email-over-imap/)
- [Parse a local EML file](https://recursiveai.org/onemail/cookbook/parse-eml/)
- [Extract attachments](https://recursiveai.org/onemail/cookbook/extract-attachments/)

## The short answer

Use `Pop3Client.ConnectAsync` with implicit TLS, log in, enumerate `UidlAsync` and call `RetrieveAsync` with each message number and an output stream. Finish with `QuitAsync`. The code above writes EML files and leaves messages on the server: it never sends a delete command.

The download includes a local test mailbox and a synthetic message, so the printed result does not depend on a personal account or a public mail provider. The fixture uses port 1995. For a real service, replace the host and port with the provider’s settings, keep credentials outside source, and use its trusted TLS certificate. OneMail’s [connection options](https://github.com/recursive-ai-oss/onemail-csharp/blob/fe2e40ab5837ca7e9e9641c1dcdfccf209094f89/src/OneMail.Client/Protocol/ProtocolConnectionOptions.cs) identify 995 as the normal implicit-TLS POP3 port.

## Does POP3 always delete emails from the server?

No. In OneMail, retrieving a message and deleting it are separate operations. `RetrieveAsync` sends RETR and writes the message to your stream. `DeleteAsync` sends DELE; deletion takes effect at QUIT. The sample calls the first operation, never the second. Its final `StatAsync` response shows that the fixture still contains one message after the download.

If your application needs deletion, make that an explicit user-selected policy after successful local storage, not a side effect of reading a message. OneMail provides `ResetAsync` for resetting pending deletions within a session. The [POP3 client documentation](https://recursiveai.org/onemail/docs/pop3/) lists the commands and their failure behavior. The protocol’s separation of retrieval, deletion marking and the update state is also specified in [RFC 1939, sections 5 and 6](https://www.rfc-editor.org/rfc/rfc1939.html#section-5).

A different mail client or a server’s retention policy may still remove messages. This sample’s behavior describes the commands it sends, not a guarantee that no other session or administrator can change the mailbox. The subject-only variation also retrieves a message without requesting deletion; it just parses the message instead of saving the raw EML file.

## How do I download an email that hasn’t been downloaded from the server?

Identify it by the server’s unique ID, not its current message number. OneMail’s `UidlAsync` result gives both a session-local number and a persistent identifier. The number tells RETR which message to fetch now. It is not a reliable filename or deduplication key across separate sessions, because message numbers are valid for one session only.

The example deliberately uses numbered filenames to keep the first download readable. A recurring importer should keep a per-account record of successfully stored UIDL identifiers, skip identifiers already present and record a new identifier only after its message is completely stored. Keep the identifier as data rather than using an untrusted server string directly as a filesystem path. Servers without UIDL return `-ERR`; this particular example requires UIDL instead of silently guessing which messages are new.

`RetrieveAsync` streams the server’s message into the destination, removes POP3 dot-stuffing and retains the line endings as sent. `GetMessageAsync` instead returns a MIME entity tree when you want to inspect content immediately. For existing EML files, use a file parser rather than opening another POP3 connection.

Only one command may run at a time on this client. Finish each retrieval before issuing the next operation, and do not share the same session between concurrent workers. A failed RETR or DELE leaves the session usable according to OneMail’s docs, but that does not make an interrupted local output file complete. Keep retry state separate from successfully saved mail.
