Table of Contents

Bodu.IO.Pst

Bodu.IO.Pst

Bodu.IO.Pst is a low-level, read-only container reader for the Outlook personal-folders format (PST / MS-PST, Unicode and ANSI formats). Part of the Binary Formats & I/O topic, it reads the node database (NDB) - the header, the node and block B-trees, block data with the format's permute and cyclic content encodings decoded and checksums verified, multi-block data trees, and per-node subnode trees - and the LTP layer over it: heap-on-node, BTree-on-heap, and per-node property-context and table-context views with wire-typed values. It carries no MAPI semantics and no writing - it is the substrate the Bodu.Formats.Outlook.Pst mail-store reader builds on, the same container/format split as Bodu.IO.Compound beneath Bodu.Formats.Excel.Binary.

A PST file is a node-oriented database in a single file. PstFile opens it as a disposable session; every object the file holds - folders, messages, tables, internal maps - is a PstNode addressed by a 32-bit PstNodeId whose five low bits carry the node's PstNodeType.

Concept Type Role
File PstFile Opens the container, walks the node B-tree, and anchors every read.
Node PstNode One object: a data payload (assembled transparently from its data tree), a private subnode tree, and the LTP views over its heap.
Property context PstPropertyContext The node's property bag: 16-bit property identifiers with wire-typed values, resolved on access.
Table context PstTableContext The node's table: typed columns over identifier-keyed rows, streamed one row block at a time.

Key concepts

Concept Plain-language meaning
Node database (NDB) The bottom layer: nodes and blocks indexed by two B-trees (NBT and BBT), with per-block trailers, checksums, and the optional content encodings.
Node identifier (NID) A 32-bit identifier - five type bits plus a 27-bit index. Well-known anchors: the message store (0x21), the name-to-id map (0x61), and the root folder (0x122).
Data tree A payload larger than one block is stored as an XBLOCK / XXBLOCK tree of data blocks; ReadAllBytes flattens it, OpenDataStream streams it one leaf at a time.
Subnode tree A node's private namespace of child nodes - a message keeps its recipient and attachment tables there.
LTP The middle layer: a heap allocated inside a node's data (heap-on-node), B-trees over that heap, and the property-context (0xBC) and table-context (0x7C) structures built on them.
Content encoding The format's "compressible encryption": a byte permutation or block-keyed cyclic substitution applied to block data, decoded transparently.
Validation level PstValidationLevel - Compatible (default), Strict (every checksum and signature enforced), Minimal.

For the full glossary, see Core concepts.

Scope and limitations

  • Read-only, both PST formats. The post-2003 Unicode format (wVer 23) and the legacy ANSI format (wVer 14/15) are read through the same surface; OST files are recognized and rejected with PstUnsupportedFormatException.
  • No MAPI semantics. Property values surface with their raw wire types; property meaning (subjects, senders, recipients) belongs to the mail-store reader layered on top.
  • Bounded memory. Payloads stream through a decoded-block LRU cache sized by BlockCacheSize; OpenDataStream keeps one leaf block resident regardless of the logical payload size.

Worked example - open, enumerate, read

  1. Probe the input cheaply with PstFile.IsPstFile(stream).
  2. Open the container: using PstFile file = PstFile.OpenRead(path).
  3. Enumerate the node directory with file.EnumerateNodes(), or resolve a well-known node directly.
  4. Read a node's property context or table context, or its raw payload.
using Bodu.IO.Pst;

using PstFile file = PstFile.OpenRead("archive.pst");

Console.WriteLine($"Format: {file.Format}, encoding: {file.CryptMethod}");

PstNode store = file.GetNode(PstNodeId.MessageStore);
foreach (PstPropertyValue value in store.ReadPropertyContext())
    Console.WriteLine($"0x{value.PropertyId:X4} (wire 0x{value.WireType:X4}): {value.RawData.Length} bytes");

Common scenarios

Scenario Reach for
Test whether a file is a PST of any variant PstFile.IsPstFile(stream)
Open a file or stream PstFile.OpenRead(path) / OpenRead(stream) / Open(stream, options)
Enforce every checksum and signature new PstFileOptions { ValidationLevel = PstValidationLevel.Strict }
Tune or disable the decoded-block cache PstFileOptions.BlockCacheSize (0 disables)
Walk the node directory file.EnumerateNodes()
Resolve a node that must exist file.GetNode(id) (throws PstNodeNotFoundException)
Resolve a node that may be absent file.TryGetNode(id, out node)
Read a node's whole payload node.ReadAllBytes()
Stream a large payload node.OpenDataStream(); node.DataLength prices it first
Read a node's property bag node.ReadPropertyContext()
Read a node's table node.ReadTableContext() → EnumerateRows() / TryGetRow(rowId, out row)
Walk a node's private children node.EnumerateSubnodes() / node.TryGetSubnode(id, out subnode)
Classify why a file was rejected catch (PstFileException ex) → ex.Error

Headline types - Bodu.IO.Pst

Type Purpose
PstFile The disposable read session: OpenRead / Open / IsPstFile, Format / CryptMethod, node enumeration and lookup.
PstNode One node: ReadAllBytes / OpenDataStream / DataLength, subnode access, and the ReadPropertyContext / ReadTableContext LTP views.
PstNodeId The 32-bit identifier - PstNodeType in the five low bits, index above; well-knowns MessageStore / NameToIdMap / RootFolder.
PstPropertyContext The property bag - tag-ordered PstPropertyValue entries with typed accessors, values resolved on access.
PstTableContext The table - PstTableColumn metadata, RowCount, streaming EnumerateRows, keyed TryGetRow.
PstFileOptions Read options - PstValidationLevel and the decoded-block BlockCacheSize.
PstFileException The common base, carrying a PstFileError category; PstFileFormatException, PstUnsupportedFormatException, and PstNodeNotFoundException derive from it.

The mail-store reader built on this package

Bodu.Formats.Outlook.Pst layers the messaging vocabulary on top: OutlookMailStore opens a .pst as a mail store and exposes the folder hierarchy, messages with decoded MAPI properties, recipients, attachments (including nested embedded messages), store-wide named-property resolution, and the text/HTML/compressed-RTF bodies - sharing the Bodu.Formats.Outlook MAPI value model with the .msg reader.

using Bodu.Formats.Outlook;

using var store = OutlookMailStore.OpenRead("archive.pst");

foreach (OutlookMailFolder folder in store.RootFolder.EnumerateSubfolders())
{
    Console.WriteLine(folder.DisplayName);
    foreach (OutlookMailMessage message in folder.EnumerateMessages())
        Console.WriteLine($"  {message.Subject} - {message.SenderName}");
}

Where to go next