Recursive AI

Convert a PST file to EML in C#

All onemail recipes

Walk the store’s folders with PstStore, convert each message with MapiToMimeConverter, and write one .eml per message. The program below exports Apache Tika’s public test store.

Terminal
dotnet new console -o PstToEml && cd PstToEml
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-microsoft-module/src/test/resources/test-documents/testPST.pst

Put this in Program.cs and run dotnet run:

C#
using RecursiveAI.OneMail.Conversion.OxcMail;
using RecursiveAI.OneMail.Pst.Messaging;
 
if (!PstStore.TryOpen("testPST.pst", out PstStore? store, out var defects))
{
    Console.WriteLine($"Not a PST: {string.Join(", ", defects)}");
    return;
}
 
var converter = new MapiToMimeConverter();
int written = 0, unresolved = 0;
using (store)
{
    foreach (PstFolder folder in store!.EnumerateFolders())
    {
        foreach (PstMessage message in folder.EnumerateMessages())
        {
            // One folder per PST folder, one .eml per message
            string dir = Path.Combine(["eml", .. folder.Path.Split('/').Select(Safe)]);
            Directory.CreateDirectory(dir);
            var result = converter.Convert(message);
            string file = Path.Combine(dir, $"{++written:D5}.eml");
            using (FileStream eml = File.Create(file))
                result.Value.WriteTo(eml);
 
            // Exchange-only senders with no SMTP address are left out and reported
            unresolved += result.Report.Count(e => e.Code == "AddressUnresolved");
            Console.WriteLine($"{file}  {new FileInfo(file).Length,7:N0} bytes");
        }
    }
}
Console.WriteLine($"{written} messages, {unresolved} unresolved addresses");
 
static string Safe(string name) =>
    string.Concat(name.Select(c =>
        Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
Text
eml/Début du fichier de données Outlook/00001.eml    5,899 bytes
eml/Début du fichier de données Outlook/00002.eml    9,006 bytes
eml/Début du fichier de données Outlook/00003.eml    4,493 bytes
eml/Début du fichier de données Outlook/00004.eml    4,745 bytes
eml/Début du fichier de données Outlook/00005.eml    5,277 bytes
eml/Début du fichier de données Outlook/00006.eml   18,368 bytes
eml/Début du fichier de données Outlook/00007.eml   21,015 bytes
7 messages, 2 unresolved addresses

Every file opens in Python’s email package and with the onemail tool. Messages that arrived over the Internet keep the headers they arrived with. The forwarded message in 00007.eml is still attached as a message. Messages are read one at a time, so a large store doesn’t have to fit in memory.

The two unresolved addresses

Message 7 was sent inside an Exchange organization. Its sender has only an Exchange (X.500) address, with no SMTP form in the store, so the .eml has no From header. The report lists it as AddressUnresolved. The project reports the same gap on its own test set: 30 such entries over 70 items.

Limits

  • A preview. 0.3.0-preview.2: the API can change before 1.0.
  • Exchange addresses. There is no directory lookup: “EX addresses with no SMTP form in the item are left out and reported”.
  • Bcc isn’t written by default (OxcMailExportOptions.IncludeBcc).
  • Mail only. This loop writes messages. To write appointments and tasks as .ics files too, convert the whole store with Mail.ConvertAsync, as in Convert a PST file to mbox.
  • Search folders and hidden items are skipped, so no message is written twice.

To write one mbox instead of many files, see Convert a PST file to mbox. One message at a time: Convert MSG to EML. Every rule: MAPI and MIME conversion.