---
title: "Extract attachments from email in C#"
url: https://recursiveai.org/onemail/cookbook/extract-attachments/
description: "Extract attachments from email in C# and save them to disk: .eml, Outlook .msg, winmail.dat, PST and mbox with one loop, file names made safe. Runnable code with OneMail 0.3.0-preview.2."
version: "0.3.0-preview.2"
---

# Extract attachments from email in C#

Open the file with `Mail.Open`, and for each message copy `attachment.Open()` to a file. The same loop works for `.eml`, Outlook `.msg`, `winmail.dat`, PST stores and mbox files. The program below saves the attachments of three public test files from Apache Tika and Apache POI.

```shell
dotnet new console -o ExtractAttachments && cd ExtractAttachments
dotnet add package RecursiveAI.OneMail --version 0.3.0-preview.2
curl -LO https://raw.githubusercontent.com/apache/tika/4.2.0/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/test/resources/test-documents/testRFC822_multipart_attachments.eml
curl -LO https://raw.githubusercontent.com/apache/poi/REL_5_4_1/test-data/hsmf/attachment_test_msg.msg
curl -LO https://raw.githubusercontent.com/apache/poi/REL_5_4_1/test-data/hmef/quick-winmail.dat
```

Put this in `Program.cs` and run `dotnet run` (or `dotnet run -- yourfile.eml`):

```csharp
using RecursiveAI.OneMail;

// Any message file or mailbox works the same: .eml, .msg, winmail.dat, .pst, mbox
string[] inputs = args.Length > 0 ? args :
[
    "testRFC822_multipart_attachments.eml",
    "attachment_test_msg.msg",
    "quick-winmail.dat",
];

foreach (string input in inputs)
{
    string dir = Path.Combine("attachments", Path.GetFileNameWithoutExtension(input));
    Directory.CreateDirectory(dir);

    using MailContainer mail = Mail.Open(input);
    foreach (IMailItem msg in mail.Enumerate())
    {
        foreach (IMailAttachment a in msg.Attachments)
        {
            // FileName is what the sender wrote: keep only the name, never a path
            string name = Path.GetFileName(a.FileName ?? "");
            if (name == "") name = "attachment.bin";
            string path = Path.Combine(dir, name);
            for (int i = 2; File.Exists(path); i++)
                path = Path.Combine(dir, $"{i}-{name}");

            using (Stream content = a.Open())   // decoded content
            using (FileStream file = File.Create(path))
                content.CopyTo(file);
            Console.WriteLine($"{path}  {new FileInfo(path).Length:N0} bytes  {a.ContentType}");
        }
    }
}
```

```text
attachments/testRFC822_multipart_attachments/Test TxtA.txt  33 bytes  text/plain
attachments/testRFC822_multipart_attachments/Test TxtB.txt  33 bytes  text/plain
attachments/attachment_test_msg/test-unicode.doc  24,064 bytes  application/msword
attachments/attachment_test_msg/pj1.txt  89 bytes  text/plain
attachments/quick-winmail/quick.doc  19,968 bytes  application/msword
attachments/quick-winmail/quick.html  428 bytes  text/html
attachments/quick-winmail/quick.pdf  18,638 bytes  application/pdf
attachments/quick-winmail/quick.txt  235 bytes  text/plain
attachments/quick-winmail/quick.xml  143 bytes  text/xml
```

`Open()` returns the decoded content: base64 and quoted-printable are undone, and Outlook attachments come straight from the file. The five files from `quick-winmail.dat` are byte-identical to the copies Apache POI keeps beside it.

## Things to know

- **Never use the name as a path.** `FileName` is what the sender wrote, and it can hold `../` or a full path. `Path.GetFileName` keeps only the name; two attachments with the same name don’t overwrite each other.
- **Inline images** (pictures the HTML body shows through `cid:` links) are attachments too, with `IsInline` set. Skip them with `if (a.IsInline) continue;`.
- **Attached emails** have `Embedded` set: read that `IMailItem` the same way to reach the files inside it.
- **winmail.dat inside an email** is unpacked for you: the message lists the files in it instead.
- **Content types.** `ContentType` is the type the message stored. When Outlook stored none, or only `application/octet-stream`, a known file extension decides it (`.pdf` is `application/pdf`) and `ContentTypeInferred` is true.

## Limits

- **A preview.** 0.3.0-preview.2: the API can change before 1.0.
- **`Size` costs a pass.** It decodes the content once to measure it, so prefer the file length after copying, as above.

Reading the rest of a message: [Parse an EML file](https://recursiveai.org/onemail/cookbook/parse-eml/), [Read an Outlook MSG file](https://recursiveai.org/onemail/cookbook/read-msg-file/). The shared view: [Mail.Open and the read view](https://recursiveai.org/onemail/docs/shared-view/).
