I build a browser tool that reads Outlook .pst and .ost files locally. After a file opens, the UI has to select some folder. For a while the rule was: select the first folder that has any messages.
On one fixture that rule picked the wrong folder, and the obvious fix (look for a folder named "Inbox") has a problem of its own. This post covers the replacement: a heuristic, what it's based on, and where it stops.
The failure
pst-extractor's public test OST is a small Outlook.com cache in the 4 KB-page layout. In its folder tree, the first folder with mail is Deleted Items (one deleted invitation). The Inbox, with ten messages, comes after it. So "first folder with mail" opened the archive on a deleted invitation.
That's one fixture. I'm not claiming every OST is ordered that way. It was enough to show that "first folder with mail" depends on an order the file doesn't promise.
Why not match the name
Special-folder display names are localized, so the Inbox isn't necessarily called "Inbox". Matching a list of translations would also match user-created folders that happen to have one of those names. I wanted a signal that doesn't come from the display name.
What the spec describes
Microsoft's Special Folders Protocol, [MS-OXOSFLD] section 2.2.3, "Binary Identification Properties", lists binary properties that each hold the entry ID of a special folder: Calendar, Contacts, Journal, Notes, Tasks, Reminders and Drafts. The copy of the spec I read (the June 2016 PDF release, v20160613) says these properties "are read from or written to either the Inbox folder or the Root folder". Per that copy, the Inbox is used for the owner's mailbox and the Root folder for a delegate's. I haven't compared that wording against the current revision of the spec.
Note what the spec does not say: that a folder carrying these properties is the Inbox. It describes where clients store the entry IDs of other folders. Using that in reverse to spot the Inbox is my inference.
The heuristic
In the project's folder-tree builder:
const NID_ROOT_FOLDER = 0x122; const SPECIAL_FOLDER_ID_PROPS = [0x36d0, 0x36d1, 0x36d2, 0x36d3, 0x36d4, 0x36d7]; function carriesSpecialFolderIds(folder: PSTFolder): boolean { const items = (folder as unknown as { pstTableItems: Map<number, unknown> | null }).pstTableItems; return SPECIAL_FOLDER_ID_PROPS.some((prop) => items?.has(prop)); } // while building each folder node: if (id !== NID_ROOT_FOLDER && carriesSpecialFolderIds(folder)) node.isInbox = true; The set covers six of the properties in that table (Calendar, Contacts, Journal, Notes, Tasks, Drafts). The root folder is excluded because the spec puts the same properties there too. Any other folder that has at least one of them is treated as the Inbox.
A few honest notes about this code:
- It reaches into
pst-extractor's internalpstTableItemsmap through a cast. That's a dependency on a library internal, and it could break on an upgrade. - I've confirmed
0x36D0(PidTagIpmAppointmentEntryId) against Microsoft's MAPI reference. The other five tag values come from the project's source, and I didn't check each of them against an official table for this post. - It flags every non-root folder that carries one of the properties. In the two fixtures below that have a marked Inbox, exactly one non-root folder qualified; the Enron fixture had none. A file where several do, or where the wrong folder does, would get a wrong guess.
The open rule, with the fallback
export function openingFolder(root: PstFolderNode): PstFolderNode | null { const rows = flattenFolders(root); return ( rows.find((row) => row.node.isInbox && row.node.messageCount > 0)?.node ?? rows.find((row) => row.node.messageCount > 0)?.node ?? null ); } Prefer the flagged Inbox only if it has mail. Otherwise fall back to the old rule. The fallback isn't a safety net I hope never to use; two of the three fixtures need it.
The three fixtures
The commit adds one test per fixture:
| Fixture | Folder(s) flagged isInbox | Opens on |
|---|---|---|
pst-extractor 4 KB-page test OST | Inbox | Inbox (10 messages), not Deleted Items |
dist-list.pst | Inbox (root not flagged) | Calendar, because the flagged Inbox is empty |
enron.pst (an exported subset) | none | first folder with mail, MLOKAY (Non-Privileged) |
According to the commit message, the test goes red without the flag. I read the test code; I didn't run it for this post.
Three fixtures is a small sample. The test assertions cover both the heuristic and the fallback; I have not rerun them here. They don't show the heuristic is right on arbitrary PST and OST files.
Takeaways
- "First folder with mail" depends on folder order. Display names depend on language. Neither is a stable signal.
- The special-folder entry IDs described in [MS-OXOSFLD] 2.2.3 give a language-independent clue for spotting the Inbox. Treat it as a heuristic you built, not a rule the spec promises.
- Exclude the root folder, which carries the same properties.
- Only prefer the flagged folder when it has mail, and keep "first folder with mail" as the fallback.
- Test on files that exercise each branch: flagged with mail, flagged but empty, and nothing flagged.
The viewer that uses this is the OST viewer. Disclosure: I built it.
Source note: I read commit 7c40c5d (2026-10-19), the current archive.ts and folder-tree.ts, and the commit's test diff on 2026-11-08. I didn't run the test suite. The [MS-OXOSFLD] quotes are from the June 2016 PDF release, not the current online revision.
