---
title: "Recover a corrupted .msg, .doc or .xls file in C#"
url: https://recursiveai.org/onearchive/cookbook/recover-compound-file/
description: "Recover a corrupted or truncated .msg, .doc or .xls file in C#: extract every stream that survives, skip or keep the damaged ones, and get a list of what was lost. A complete program with OneArchive 1.1."
version: "1.1.1"
---

# Recover a corrupted .msg, .doc or .xls file in C#

Extract the file with `CompoundFile = new CompoundFileReaderOptions { Recovery = true }`: streams that survive are written as usual, and `DamagedEntries` decides what happens to the ones that were cut short. `OnDamagedEntry` reports each one, and the result counts them. The program below cuts a real Outlook message short, shows the normal error, then gets the subject, sender and body back.

Outlook `.msg` files, legacy Office `.doc`, `.xls` and `.ppt` files, and `.msi` installers are all Microsoft compound files: a small file system of storages and streams inside one file. A copy or download that stops early, or a bad sector, breaks its tables, and a strict reader refuses the whole file. Recovery mode is off by default.

```shell
dotnet new console -o RecoverMsg && cd RecoverMsg
dotnet add package RecursiveAI.OneArchive --version 1.1.1
curl -LO https://raw.githubusercontent.com/apache/poi/REL_5_3_0/test-data/hsmf/quick.msg
```

`quick.msg` is a sample email from the Apache POI test files. Put this in `Program.cs` and run `dotnet run`:

```csharp
using System.Text;
using RecursiveAI.OneArchive;
using RecursiveAI.OneArchive.Cfb;
using RecursiveAI.OneCompress;

// Damage the message: keep the first three quarters, as an interrupted copy would.
byte[] msg = File.ReadAllBytes("quick.msg");
int cut = msg.Length * 3 / 4;
File.WriteAllBytes("damaged.msg", msg[..cut]);
Console.WriteLine($"quick.msg: {msg.Length:N0} bytes; damaged.msg: {cut:N0} bytes");

// Without recovery, the damaged file is a typed error.
try
{
    Archive.Extract("damaged.msg", "strict");
}
catch (CompressionFormatException e)
{
    Console.WriteLine($"without recovery: {e.Message}");
}

// With recovery, extract every stream that survives and skip the damaged ones.
var options = new ArchiveExtractOptions
{
    CompoundFile = new CompoundFileReaderOptions { Recovery = true },
    DamagedEntries = DamagedEntryPolicy.Skip,
    OnDamagedEntry = e => Console.WriteLine(
        $"  damaged: {e.Name} ({e.Size} of {((CfbEntry)e).DeclaredSize} bytes left)"),
};
var result = Archive.Extract("damaged.msg", "recovered", options);
Console.WriteLine($"entries written: {result.EntriesExtracted}, damaged and skipped: {result.EntriesDamaged}");

// Outlook keeps each text property in a stream of its own; 001E is 8-bit text.
string Read(string id)
{
    string file = $"recovered/__substg1.0_{id}001E";
    if (!File.Exists(file)) return "(lost)";
    return Encoding.Latin1.GetString(File.ReadAllBytes(file)).Trim('\0', '\r', '\n');
}
Console.WriteLine($"  Subject: {Read("0037")}");
Console.WriteLine($"  From:    {Read("0C1A")}");
Console.WriteLine($"  Body:    {Read("1000")}");
```

```text
quick.msg: 17,920 bytes; damaged.msg: 13,440 bytes
without recovery: cfb: invalid data at byte offset 2144: expected the directory chain to stay inside the file (the file has 25 sectors after the header; found sector 0x00000019)
  damaged: __substg1.0_00020102 (0 of 16 bytes left)
  damaged: __substg1.0_00030102 (0 of 72 bytes left)
  damaged: __properties_version1.0 (320 of 1328 bytes left)
  damaged: __recip_version1.0_#00000000/__substg1.0_0FF60102 (0 of 4 bytes left)
  damaged: __recip_version1.0_#00000000/__substg1.0_0FFF0102 (0 of 109 bytes left)
  damaged: __recip_version1.0_#00000000/__substg1.0_3001001E (0 of 11 bytes left)
  damaged: __recip_version1.0_#00000000/__substg1.0_3002001E (0 of 2 bytes left)
  damaged: __recip_version1.0_#00000000/__substg1.0_3003001E (0 of 80 bytes left)
  damaged: __recip_version1.0_#00000000/__substg1.0_300B0102 (0 of 84 bytes left)
  damaged: __recip_version1.0_#00000000/__substg1.0_39FE001E (0 of 24 bytes left)
  damaged: __recip_version1.0_#00000000/__substg1.0_3A20001E (0 of 11 bytes left)
  damaged: __recip_version1.0_#00000000/__substg1.0_5FF6001E (0 of 11 bytes left)
  damaged: __recip_version1.0_#00000000/__substg1.0_5FF70102 (0 of 109 bytes left)
  damaged: __recip_version1.0_#00000000/__substg1.0_6001001E (0 of 15 bytes left)
  damaged: __recip_version1.0_#00000000/__properties_version1.0 (0 of 392 bytes left)
entries written: 41, damaged and skipped: 15
  Subject: Test the content transformer
  From:    Kevin Roast
  Body:    The quick brown fox jumps over the lazy dog
```

The subject, sender and body survive. The 15 streams that lost their sectors, the recipient’s address among them, are reported and not written, so nothing on disk is a cut-short copy. The 41 entries written are 39 streams and the two storages that hold them.

## Limits

- **It reads, it doesn’t repair.** You get the streams back, not a fixed `.msg` or `.doc`: bytes that are gone stay gone. Turning streams into an email or a document is up to a format-specific library.
- **How much comes back depends on where the damage falls.** With no usable header, no usable FAT sector, or no directory even after scanning, recovery still throws `CompressionFormatException`. A file that keeps its FAT and directory at the end, as some `.doc` files do, loses both when it is cut short.
- **Damage inside a stream’s bytes isn’t detected.** Recovery follows the file’s tables; a stream whose sectors are all present but overwritten reads as intact.

## Good to know

- `DamagedEntries` is `Extract` by default, which writes the shortened streams; `Skip` writes nothing for them, and `Fail` throws `CompressionFormatException` naming the first one. In every mode `result.EntriesDamaged` and `result.DamagedEntryNames` (the first 1,000) list them.
- Without `Recovery` nothing changes, and a file that passes the strict checks reads the same with it on: recovery only starts when the strict reader refuses the file.
- A damaged stream keeps the bytes before the first break in its chain: `Size` is what could be read, `CfbEntry.DeclaredSize` what the directory says, and `CfbEntry.IsDamaged` is true.
- To read streams without writing them, open a `CompoundFileReader` with the same options; `CompoundFileReader.Damage` then lists every problem found, with its kind, offset and reason.
- Recovery is bounded: `MaxRecoveryBytes` (1 GiB by default) caps the bytes it scans, and limit exceptions are never recovered from.

What is repaired and how is in [compound files](https://recursiveai.org/onearchive/docs/cfb/#recovery-choice) and [damaged entries](https://recursiveai.org/onearchive/docs/archive/#damaged-entries). Reading an intact file is in [Read and write compound files](https://recursiveai.org/onearchive/cookbook/compound-files/), and a damaged zip in [Recover files from a corrupted zip](https://recursiveai.org/onearchive/cookbook/zip-recovery/).
