From [MS-PST] sections 2.6.1.3, 2.6.2, 2.6.3 and 2.7 (the learn.microsoft.com PDF, read 2026-11-13). The node-database operations of section 2.6.1.1 and 2.6.1.2 (allocating, creating pages and blocks, creating, modifying and deleting nodes and subnodes) are on the NDB page; the structures are on the LTP page. Each operation in the specification is a table of required, recommended and optional actions and side effects; the required points are given here.
Special considerations for the node database
- Immutability. Except for the header and the allocation metadata pages, data in the NDB must never be modified in place: a new copy is written at a new location, and the old data is purged only when nothing refers to it any more.
- Single-instance storage. Data blocks have reference counts, so several nodes can hold the same block ID. With immutability, a modified copy goes to a new block ID while the unmodified copies keep the old data (this is how a folder or message is copied cheaply: the new node refers to the same blocks).
- Transactional behaviour. Because every change gives a new block ID (a node holds only a block ID for its data and optionally one for its subnodes), a reader that caches the IDs when it opens a message can tell whether the message changed since. Writers should update block IDs only after all the data is written. The
fAMapValidflag in ROOT can also guard a group of operations that need several allocations. - Backfilling (optional): when file utilisation drops below a threshold the implementation chooses (it is not in the file), it clears
DFL_BACKFILL_COMPLETEin the density list and setsulCurrentPageof the DLISTPAGE to the last AMap page; later allocations scan backwards from that page, movingulCurrentPageto the previous AMap page when one cannot serve a request; at the first AMap page the flag is set again. - Fragmentation. Because everything that is edited moves, the file fragments and has poor locality (larger streams especially, since only the edited blocks move); implementations should design access to cope with scattered reads and writes, and should cache pages and blocks (the header, AMaps and the NBT and BBT pages change with almost every operation).
- Crash recovery and AMap rebuild. Before any operation that allocates or frees space, a writer sets
fAMapValidtoINVALID_AMAPand sets it back to valid when finished. If the file was closed abnormally the flag is still invalid and the next allocation needs an AMap rebuild: mark every AMap free, then walk the NBT and BBT and mark each page and block allocated, also marking the space of the AMaps, PMaps, FMaps and FPMaps themselves. Rebuilding is not required, but an implementation that does not rebuild must check the flag before touching any map and must not modify the file at all if it is invalid; a read-only implementation may ignore the flag.
Operations at the LTP layer
Heap node (HN)
- Create: create a node whose data contains a correctly initialised
HNHDRandHNPAGEMAP. - Allocate: an allocation must fit in one block; the largest is 3580 bytes. Maintain the
HNPAGEMAPof every changed block and the fill-level map of the block (for a heap with a data tree). When the heap no longer fits one block a data tree is built, and every added block needs the right header (HNHDR,HNPAGEHDRorHNBITMAPHDR). - Free: close the gap by moving later allocations up and update
rgibAlloc(an allocation's size is the next offset minus its offset, so the heap must stay packed) and the fill-level map. - Delete: delete the node.
B-tree-on-heap (BTH)
- Create: allocate and initialise the
BTHHEADERfrom the heap. Insert: allocate for the new data and add a record; keep index and leaf blocks (HN allocations of 3580 bytes) below 90 percent full (a recommendation); a full block is split and a new index level is made, and the tree re-balanced. - Modify an entry: like modifying node data. Delete an entry: remove the record; if the leaf is not empty update the parent index key to the leaf's next key, but if the leaf is empty remove the parent row (which can cascade up). Delete a BTH: walk the entries recursively, free all heap allocations, then the header.
Property context (PC)
- Create: a BTH whose key is 2 bytes and data 6 bytes, with
hidUserRootset to the HID of the BTH header. - Insert or modify: as for a BTH. A value of up to 4 bytes sits in the record; a variable-size value of up to 3580 bytes goes in its own heap allocation (its HID in
dwValueHnid); a larger one goes in a subnode (its NID indwValueHnid). HIDs and subnode NIDs never collide because an HID is a NID of type HID. Delete a property: as deleting a BTH entry, freeing the allocation or subnode.
Table context (TC)
- Create: a heap with a
TCINFOandTCOLDESCarray and an embedded BTH set up forTCROWID. TheiBitfields must give the proper order of columns by data size, andcbDatafollows the rules for variable-size data and fixed-size data over 8 bytes (an HNID is stored). - Insert a row: append a correctly formatted row record to the row matrix and a
TCROWIDto the embedded BTH. A variable-size value of up to 3580 bytes goes in a heap allocation (HID in the 4-byte slot), a larger one in a subnode (NID in the slot), fixed-size data over 8 bytes in a heap allocation. - Add a column: update
TCINFO, add aTCOLDESC, widen every row by a data slot (and widen the existence bitmap, the CEB, if its bits run out), mark the new column "non-existent" in every row's CEB, and re-assign theiBitof any shifted column. Delete a column value: clear its CEB bit in that row. Do not delete columns. - Delete a row: remove it from the embedded BTH, move later rows up and update their
RowIndexin theTCROWIDrecords. Delete a TC: delete the node (take care that higher-level objects do not depend on it).
Operations at the messaging layer
Every operation below that changes a folder or message must also queue a search update descriptor (SUD) to the search management queue, of the type shown, and must update the parent's properties and the duplicated table rows (see templates) so that all copies agree. Calculated properties must be converted as in the calculated properties.
| Operation | Required | SUD type |
|---|---|---|
| Create the message store | a PC with NID NID_MESSAGE_STORE and the required properties; exactly one store per file | none |
| Create a folder | one PC (node type normal folder) and three TCs (node types hierarchy, contents, FAI contents); all four have the same nidIndex and must exist; the required folder PC properties and the template columns | SUDT_FLD_ADD |
| Modify folder properties | change the PC; update properties duplicated in the parent's hierarchy table | SUDT_FLD_MOD |
| Add a sub-folder | create the folder; set its nidParent in the NBT to the parent; add a row to the parent's hierarchy TC; update parent properties (such as the folder count) | SUDT_FLD_ADD |
| Move a folder | delete the row from the old parent's hierarchy TC and add one to the new parent's; set nidParent to the new parent; update both parents | SUDT_FLD_MOV |
| Copy a folder | create a new folder PC with the required properties; duplicate the three TC nodes of the original (single-instanced); add a hierarchy row to the target; new nidParent is the target | SUDT_FLD_ADD |
| Add a message | create the message PC with its required properties, and a recipient TC in a subnode; add a row to the folder's contents TC; update the folder's counts, which may cascade up to the grandparent's hierarchy row | SUDT_MSG_ADD |
| Copy a message | duplicate the message PC node (single-instanced) with nidParent the target folder; add a contents row to the target; update its properties | SUDT_MSG_ADD |
| Move a message | delete the contents row from the old folder, add one to the new, set nidParent, update both folders | SUDT_MSG_MOV |
| Delete a sub-folder | delete its hierarchy row in the parent, update the parent, delete the PC and the three TCs; delete child folders and messages recursively first. The root folder cannot be deleted | SUDT_FLD_DEL |
| Delete a message | delete its contents row, update the folder, delete the message PC | SUDT_MSG_DEL |
| Modify message properties | change the PC; if a property is cached in the parent's contents TC, update the row too | SUDT_MSG_MOD, and SUDT_MSG_ROW_MOD if the contents row changes |
| Add, modify or delete a recipient | add, change or delete a row in the message's recipient TC (with the required columns); update the message properties such as the recipient count | SUDT_MSG_MOD |
| Add an attachment | create the attachment TC in the subnode tree if it does not exist yet, create the attachment PC and add a row; update the message's attachment properties | SUDT_MSG_MOD |
| Modify or delete an attachment | change or delete the attachment PC and update the duplicated attachment table row; update the message | SUDT_MSG_MOD |
| Create the name-to-ID map | a PC with a special NID and the single property PidTagNameidBucketCount = 251 (0xFB); the map must exist | none |
| Add a named property | add or change PidTagNameidStreamEntry, add or change PidTagNameidStreamString or PidTagNameidStreamGuid as the type needs, and the hash bucket properties. Do not remove named properties | none |
The minimal PST
An implementation should treat a file as invalid if any of these nodes is missing or malformed. NIDs in the first column are fixed values; the folder NIDs here are samples and can be any valid NID of the type.
| NID | Type | Special NID or object | Kind | Minimal state |
|---|---|---|---|---|
| 0x0021 | internal | NID_MESSAGE_STORE | PC | required properties |
| 0x0061 | internal | NID_NAME_TO_ID_MAP | PC | empty (only the bucket count) |
| 0x0122 | normal folder | NID_ROOT_FOLDER | PC | schema properties |
| 0x012D | hierarchy table | root folder | TC | 2 rows (the specification's table says 2; its root folder text says 3 rows, "IPM SuBTree", "Search Root" and "Spam Search Folder") |
| 0x012E | contents table | root folder | TC | columns only |
| 0x012F | FAI contents table | root folder | TC | columns only |
| 0x01E1 | internal | NID_SEARCH_MANAGEMENT_QUEUE | queue node | empty queue |
| 0x0201 | internal | NID_SEARCH_ACTIVITY_LIST | node | empty |
| 0x060D | hierarchy table | NID_HIERARCHY_TABLE_TEMPLATE | TC | columns only |
| 0x060E | contents table | NID_CONTENTS_TABLE_TEMPLATE | TC | columns only |
| 0x060F | FAI contents table | NID_ASSOC_CONTENTS_TABLE_TEMPLATE | TC | columns only |
| 0x0610 | search contents table | NID_SEARCH_CONTENTS_TABLE_TEMPLATE | TC | columns only |
| 0x0692 | recipient table | NID_RECIPIENT_TABLE | TC | columns only |
| 0x0671 | attachment table | NID_ATTACHMENT_TABLE | TC | columns only |
| 0x2223 | search folder | Spam search folder | PC | columns only |
| 0x8022 | normal folder | IPM subtree | PC | schema properties |
| 0x802D, 0x802E, 0x802F | hierarchy, contents, FAI contents | IPM subtree | TC | 2 rows, columns only, columns only |
| 0x8042 | normal folder | search root | PC | schema properties |
| 0x804D, 0x804E, 0x804F | hierarchy, contents, FAI contents | search root | TC | columns only |
| 0x8062 | normal folder | Deleted Items | PC | schema properties |
| 0x806D, 0x806E, 0x806F | hierarchy, contents, FAI contents | Deleted Items | TC | columns only |
The hierarchy is: root folder, with the top of personal folders (IPM subtree), the search root and the spam search folder under it; Deleted Items under the IPM subtree. Name-to-ID map: a PC with one property, PidTagNameidBucketCount = 251.
The template objects are the six empty TCs above (columns on the templates page). The minimal search objects are an empty search management queue and an empty search activity list.
| Folder | nidParent | 0x3001 display name | 0x3602 content count | 0x3603 unread | 0x360A subfolders | Hierarchy TC rows |
|---|---|---|---|---|---|---|
| Root | itself | empty string | 3 | 0 | true | 3: "IPM SuBTree", "Search Root", "Spam Search Folder" |
| Top of Personal Folders (IPM subtree) | root | Top of Personal Folders | 1 | 0 | true | 1: "Deleted Items" |
| Search Root | root | Search Root | 0 | 0 | false | none |
| Spam search folder | root | SPAM Search Folder 2 | 0 | 0 | false | none |
| Deleted Items | IPM subtree | Deleted Items | 0 | 0 | false | none |
Each folder's contents TC and FAI contents TC have the template columns and no rows. The specification gives the minimal state of the root hierarchy table as "2 rows" in the node list and "3 rows" in the root folder description; the node list says the same of the IPM subtree table (2 rows) while the folder description gives one row.