API Reference · version 2.0.0.0 · fully managed .NET PST/OST library — read, write, merge, split, repair & validate
Bastion.Pst is a fully managed .NET library for working with
Microsoft Outlook data files — .pst and .ost — without
Outlook, without MAPI, and without any external runtime. It reads both legacy ANSI and modern
Unicode stores and writes standards-compliant Unicode PST files that open cleanly in every version
of Outlook from Outlook 2003 onwards (Outlook 2003, 2007, 2010, 2013, 2016, 2019, 2021 and
Microsoft 365). The Unicode PST format was introduced in Outlook 2003; Outlook 2002 and
earlier read only the legacy ANSI format, so Bastion PST SDK will not silently upgrade an ANSI store to
Unicode without your explicit consent (see the ANSI → Unicode safety gate).
clsStoreImporter): into an existing store or a
brand-new one, with the target folder path created on demand and folder counts maintained.
ImportOlm mirrors the OLM folder tree.Bastion.Pst.Authoring).store.Views.Mail(), .Contacts(), .Appointments(),
.Tasks(), .Notes(), .Rss()): flat rows a grid, card view
or scheduler binds to directly, with folder classification, Outlook's hidden address-book
caches, contact groups (distribution lists, with their members decoded) and
duplicate-contact folding handled once, in the library. MailIn lists a
single folder from its contents table alone, and clsItemQuery filters
before an item is opened — the difference between reading a few hundred items and
reading a hundred thousand (Bastion.Pst.Views).clsItemOps, which knows that a source is never written
to, that a legacy ANSI store never accepts data, and that every write is an atomic,
reopen-validated rewrite.clsPstEvents.Default for store
opens, read progress (Current/Total/Percent), folder
selections, closes and errors from anywhere in the library: no polling, and no callback threaded
through every call.clsStoreBuilder.CreateEmpty), or clone just the folder hierarchy
of an existing store (clsStoreImporter.CloneStructure).CancellationToken and stop cleanly (the atomic-write model leaves nothing partial);
and an ANSI → Unicode safety gate refuses to silently upgrade a
legacy ANSI store (which would make it unopenable in Outlook 2002 and earlier) without your
explicit consent. A cheap header-only clsPersonalStorage.PeekFormat /
IsAnsi lets you detect the format up front.ErrorCode is
0 on success and a documented number otherwise, alongside a human-readable
ErrorDescription. See Quick Start.[MS-PST] file format.Bastion PST SDK always writes the Unicode PST format (the modern large-file format, introduced with Outlook 2003). Every Outlook release from 2003 onward opens the files it produces:
| Outlook version | Year | Opens Bastion PST SDK output (Unicode PST)? |
|---|---|---|
| Outlook 97 | 1997 | No — ANSI-only |
| Outlook 98 | 1998 | No — ANSI-only |
| Outlook 2000 | 1999 | No — ANSI-only |
| Outlook 2002 / XP | 2001 | No — ANSI-only |
| Outlook 2003 | 2003 | Yes — Unicode introduced here |
| Outlook 2007 | 2007 | Yes |
| Outlook 2010 | 2010 | Yes |
| Outlook 2013 | 2013 | Yes |
| Outlook 2016 | 2016 | Yes |
| Outlook 2019 | 2018 | Yes |
| Outlook 2021 | 2021 | Yes |
| Outlook 2024 | 2024 | Yes |
| Outlook for Microsoft 365 | current | Yes |
It will not open in Outlook 2002/XP or earlier — those releases only understand the legacy ANSI PST format (2 GB ceiling) and cannot read a Unicode PST. If you must target one of those, the file is not compatible; there is no ANSI-write mode. Note the reverse works: Bastion PST SDK happily reads those old ANSI PST/OST files and converts them to Unicode. (The classic “new Outlook” Windows app and Outlook on the web do not use on-disk PST files at all, so this table concerns desktop Outlook.)
Reference the assembly under lib/ that matches your project. The API is identical
across all of them.
| Assembly | Runtime | Use from |
|---|---|---|
lib/net46 … lib/net481 | .NET Framework 4.6, 4.6.1, 4.6.2, 4.7, 4.7.1, 4.7.2, 4.8, 4.8.1 | .NET Framework apps — a native build for your exact version |
lib/netcoreapp2.0 … lib/netcoreapp3.1 | .NET Core 2.0, 2.1, 3.0, 3.1 | .NET Core apps |
lib/net5.0 … lib/net10.0 | .NET 5, 6, 7, 8 (LTS), 9, 10 (LTS) | Modern .NET apps, Windows/Linux/macOS |
Eighteen builds, one per framework. There is no netstandard build:
each runtime gets a native assembly instead of a shim. A WinForms app targeting
net8.0-windows uses lib/net8.0 — the library is portable and needs no
Windows-specific build.
On .NET Core and .NET 5+ also add the Microsoft package
System.Text.Encoding.CodePages (for correct ANSI→Unicode transcoding). The eight
.NET Framework builds need nothing beyond the framework: those code pages are
built in, and the assembly does not reference the package at all, so
Bastion.Pst.dll can simply be dropped next to your executable.
Because it is an ordinary managed assembly with no native parts, Bastion PST SDK is also a practical
PowerShell library — useful for auditing a directory of archives, exporting a
mailbox, or merging a decade of PST files from a scheduled job. Load
lib\net48 from Windows PowerShell 5.1 and lib\net8.0 from
PowerShell 7+. The distribution ships a ready-made module and fourteen worked tasks in
samples/PowerShell; see example 32.
| Website | bastionsoftwaresolutions.com |
|---|---|
| Support | support@bastionsoftwaresolutions.com |
| Namespaces | Bastion.Pst.Messaging (read API),
Bastion.Pst.Views (bindable read model, queries, item operations),
Bastion.Pst.Convert (merge / split / convert),
Bastion.Pst.Authoring (create / edit),
Bastion.Pst.Import / .Export,
Bastion.Pst.Validation, Bastion.Pst.Repair,
Bastion.Pst.Diagnostics |
© 2026 Bastion Software Solutions Ltd — bastionsoftwaresolutions.com. This page documents the public API only; internal types are intentionally omitted.
Evaluation build. The library runs as a thirty-day evaluation with every feature available. During the evaluation, input .pst and .ost files are limited to 100 MB, and merged or stacked output is split into 500 MB volumes instead of the 50 GB a licensed build uses.
Every example below shows the same operation in VB.NET and C#,
and every one demonstrates the return-code pattern — check ErrorCode first, then
read the human-readable description. The library never throws into your code, so you never need a
Try/Catch around a call for control flow.
VB.NET
Imports Bastion.Pst ' enmErrorCode, clsOperationResult
Imports Bastion.Pst.Convert ' clsStoreMerger, clsDedup, enmMergeLayout
Imports Bastion.Pst.Diagnostics ' clsDiagnostics
Imports Bastion.Pst.Messaging ' enmItemKind, clsPersonalStorage
C#
using Bastion.Pst; // enmErrorCode, clsOperationResult
using Bastion.Pst.Convert; // clsStoreMerger, clsDedup, enmMergeLayout
using Bastion.Pst.Diagnostics; // clsDiagnostics
using Bastion.Pst.Messaging; // enmItemKind, clsPersonalStorage
Merge several PSTs into one. Each source keeps its own folder tree under the destination root.
overwrite:=True replaces the destination if it already exists (otherwise you get
ErrorCode 7 DestinationExists).
VB.NET
Dim report = clsStoreMerger.Merge({"a.pst", "b.pst", "c.pst"}, "merged.pst", overwrite:=True)
' --- return-code handling (applies to every operation in the library) ---
If report.ErrorCode <> enmErrorCode.Ok Then
Console.WriteLine("Error number : " & CInt(report.ErrorCode)) ' e.g. 3
Console.WriteLine("Error text : " & report.ErrorDescription) ' "The file is locked ..."
Console.WriteLine("Combined : " & report.ToString()) ' "3: The file is locked ..."
Console.WriteLine("Technical : " & report.Detail) ' full detail for support
Return
End If
Console.WriteLine($"{report.TotalNodes:N0} items written to merged.pst.")
C#
var report = clsStoreMerger.Merge(new[] { "a.pst", "b.pst", "c.pst" }, "merged.pst", overwrite: true);
// --- return-code handling (applies to every operation in the library) ---
if (report.ErrorCode != enmErrorCode.Ok)
{
Console.WriteLine("Error number : " + (int)report.ErrorCode); // e.g. 3
Console.WriteLine("Error text : " + report.ErrorDescription); // "The file is locked ..."
Console.WriteLine("Combined : " + report.ToString()); // "3: The file is locked ..."
Console.WriteLine("Technical : " + report.Detail); // full detail for support
return;
}
Console.WriteLine($"{report.TotalNodes:N0} items written to merged.pst.");
report.Succeeded is a convenience boolean equal to ErrorCode == Ok.
The full list of numbers is in the enmErrorCode
reference below.
Unified blends same-named folders across all sources into one (one Inbox, one Sent Items).
Passing a clsDedup removes duplicate messages; rootName sets the name Outlook
shows for the mounted store; pruneEmpty drops folders left empty; blank “junk”
messages are removed by default and written to the user log.
VB.NET
Dim diag As New clsDiagnostics With {.CreateUserLog = True, .RecoverFromCorruption = True}
Dim report = clsStoreMerger.Merge(
{"2019.pst", "2020.pst", "2021.pst"}, "combined.pst",
dedup:=New clsDedup(),
diag:=diag,
overwrite:=True,
layout:=enmMergeLayout.Unified,
rootName:="All Mail 2019-2021",
pruneEmpty:=True)
If report.ErrorCode = enmErrorCode.Ok Then
Console.WriteLine($"OK - {report.DuplicatesRemoved:N0} duplicates, " &
$"{report.BlankMessagesRemoved:N0} blank and " &
$"{report.FoldersPruned:N0} empty folders removed.")
Console.WriteLine($"Activity log: {diag.UserLogPath}")
Else
Console.WriteLine(report.ToString())
End If
C#
var diag = new clsDiagnostics { CreateUserLog = true, RecoverFromCorruption = true };
var report = clsStoreMerger.Merge(
new[] { "2019.pst", "2020.pst", "2021.pst" }, "combined.pst",
dedup: new clsDedup(),
diag: diag,
overwrite: true,
layout: enmMergeLayout.Unified,
rootName: "All Mail 2019-2021",
pruneEmpty: true);
if (report.ErrorCode == enmErrorCode.Ok)
{
Console.WriteLine($"OK - {report.DuplicatesRemoved:N0} duplicates, " +
$"{report.BlankMessagesRemoved:N0} blank and " +
$"{report.FoldersPruned:N0} empty folders removed.");
Console.WriteLine($"Activity log: {diag.UserLogPath}");
}
else
{
Console.WriteLine(report.ToString());
}
Pass a set of enmItemKind values to keep only those (here: mail and contacts);
everything else is filtered out and counted in report.FilteredOut.
VB.NET
Dim kinds As New HashSet(Of enmItemKind) From {enmItemKind.Mail, enmItemKind.Contact}
Dim report = clsStoreMerger.Merge({"source.pst"}, "mail-and-contacts.pst",
keepKinds:=kinds, overwrite:=True)
Console.WriteLine($"{report.MessagesMerged:N0} kept, {report.FilteredOut:N0} filtered out.")
C#
var kinds = new HashSet<enmItemKind> { enmItemKind.Mail, enmItemKind.Contact };
var report = clsStoreMerger.Merge(new[] { "source.pst" }, "mail-and-contacts.pst",
keepKinds: kinds, overwrite: true);
Console.WriteLine($"{report.MessagesMerged:N0} kept, {report.FilteredOut:N0} filtered out.");
For a byte-faithful archive, turn the cleanups off.
VB.NET
Dim report = clsStoreMerger.Merge({"a.pst", "b.pst"}, "verbatim.pst",
overwrite:=True,
pruneEmpty:=False,
dropBlankMessages:=False) ' keep blank/empty messages too
C#
var report = clsStoreMerger.Merge(new[] { "a.pst", "b.pst" }, "verbatim.pst",
overwrite: true,
pruneEmpty: false,
dropBlankMessages: false); // keep blank/empty messages too
When the combined data would exceed a size you want per file, MergeToVolumes writes a
numbered series of PSTs, each capped at capBytes. The result carries per-volume details.
VB.NET
Dim vol = clsStoreMerger.MergeToVolumes(
{"big1.pst", "big2.pst"}, "archive.pst",
capBytes:=10L * 1024 * 1024 * 1024, ' 10 GB per volume
dedup:=New clsDedup(),
overwrite:=True,
rootName:="Archive")
If vol.ErrorCode <> enmErrorCode.Ok Then
Console.WriteLine(vol.ToString())
Else
Console.WriteLine($"{vol.Volumes.Count} volume(s), {vol.TotalMessages:N0} messages.")
For Each v In vol.Volumes
Console.WriteLine($" {v.Path}: {v.Messages:N0} msgs, {v.SizeBytes / 1048576.0:N0} MB")
Next
End If
C#
var vol = clsStoreMerger.MergeToVolumes(
new[] { "big1.pst", "big2.pst" }, "archive.pst",
capBytes: 10L * 1024 * 1024 * 1024, // 10 GB per volume
dedup: new clsDedup(),
overwrite: true,
rootName: "Archive");
if (vol.ErrorCode != enmErrorCode.Ok)
{
Console.WriteLine(vol.ToString());
}
else
{
Console.WriteLine($"{vol.Volumes.Count} volume(s), {vol.TotalMessages:N0} messages.");
foreach (var v in vol.Volumes)
Console.WriteLine($" {v.Path}: {v.Messages:N0} msgs, {v.SizeBytes / 1048576.0:N0} MB");
}
| Parameter | Type | Default | Effect |
|---|---|---|---|
sourcePaths | IList(Of String) | — | The PST/OST files to merge (one or many). |
outPath | String | — | Destination Unicode PST. |
dedup | clsDedup | Nothing | Pass New clsDedup() to remove duplicate messages (keep-first by Internet Message-ID, falling back to subject + sender + sent-time). Scoped to each destination folder: the same message filed in two different folders is kept in both, so the folder tree you put in is the folder tree you get back. Set AcrossFolders = True to collapse duplicates across the whole output instead — this compacts the folder tree, leaving folders empty whose contents existed elsewhere (and deleting them outright if pruneEmpty is on). Only offer it where the person choosing has been told that plainly. |
keepKinds | HashSet(Of enmItemKind) | Nothing | Keep only these item types; Nothing keeps all. |
pruneEmpty | Boolean | False | Remove folders that end up empty. Off by default — empty folders carry through. A folder the user created and left empty is part of their filing; a merge does not remove it unless asked. Note that search folders are never carried into the output whatever this is set to: a search folder is a saved query over other folders, not a container of mail, and its results live in the real folders, which are merged. |
diag | clsDiagnostics | Nothing | Progress events, user log, corruption recovery. |
overwrite | Boolean | False | Replace the destination if it exists. |
maxBytes | Long | 0 | Hard output size guard (0 = format ceiling). |
layout | enmMergeLayout | Stacked | Stacked or Unified. |
rootName | String | Nothing | Store name Outlook shows; defaults to “Stacked Merge”/“Unified Merge”. |
pruneEmpty | Boolean | False | Remove folders that end up empty. |
dropBlankMessages | Boolean | True | Remove provably-empty messages (logged); set False to keep everything. |
The same Merge / MergeToVolumes methods cover a wide range of tasks.
These VB.NET snippets assume the imports shown at the top.
VB.NET — live progress and corruption recovery
Dim diag As New clsDiagnostics With {.RecoverFromCorruption = True, .CreateUserLog = True}
AddHandler diag.WriteProgress, Sub(s, ev) Console.Write($"{vbCr}Writing {ev.PercentComplete}% ")
AddHandler diag.ErrorOccurred, Sub(s, ev) Console.WriteLine($" recovered: {ev.Message}")
Dim report = clsStoreMerger.Merge({"a.pst", "b.pst"}, "merged.pst", diag:=diag, overwrite:=True)
Console.WriteLine()
Console.WriteLine(If(report.Succeeded, "Done.", report.ToString()))
VB.NET — cap output size, and fall back to volumes if it won't fit
Dim sources = {"2019.pst", "2020.pst", "2021.pst"}
Dim cap As Long = 20L * 1024 * 1024 * 1024 ' 20 GB
Dim report = clsStoreMerger.Merge(sources, "merged.pst",
dedup:=New clsDedup(), overwrite:=True, maxBytes:=cap)
If report.ErrorCode = enmErrorCode.OutputSizeExceeded Then
Console.WriteLine("Too big for one file - writing 20 GB volumes instead.")
Dim vol = clsStoreMerger.MergeToVolumes(sources, "merged.pst", cap,
dedup:=New clsDedup(), overwrite:=True)
Console.WriteLine(If(vol.Succeeded, $"{vol.Volumes.Count} volume(s) written.", vol.ToString()))
ElseIf Not report.Succeeded Then
Console.WriteLine(report.ToString())
End If
VB.NET — clean up a single PST (dedup + drop blanks + prune empties, keep its name)
Dim report = clsStoreMerger.Merge({"messy.pst"}, "clean.pst",
dedup:=New clsDedup(),
overwrite:=True,
pruneEmpty:=True) ' single source keeps its original store name
Console.WriteLine($"{report.DuplicatesRemoved:N0} duplicates, " &
$"{report.BlankMessagesRemoved:N0} blank, " &
$"{report.FoldersPruned:N0} empty folders removed.")
VB.NET — merge, then verify the result is complete and valid
Imports Bastion.Pst.Validation ' clsPstHealth
Dim report = clsStoreMerger.Merge(sources, "merged.pst", dedup:=New clsDedup(), overwrite:=True)
If report.Succeeded Then
Dim health = clsPstHealth.Check("merged.pst")
Console.WriteLine(If(health.IsCorrupted, "PROBLEM: " & health.ToString(), "Output verified OK."))
End If
VB.NET — read every figure the merge report carries
Dim report = clsStoreMerger.Merge(sources, "merged.pst", dedup:=New clsDedup(),
overwrite:=True, layout:=enmMergeLayout.Unified, pruneEmpty:=True)
Console.WriteLine($"code : {CInt(report.ErrorCode)} ({report.ErrorDescription})")
Console.WriteLine($"items written : {report.TotalNodes:N0}")
Console.WriteLine($"messages merged : {report.MessagesMerged:N0}")
Console.WriteLine($"duplicates : {report.DuplicatesRemoved:N0}")
Console.WriteLine($"blank removed : {report.BlankMessagesRemoved:N0}")
Console.WriteLine($"filtered out : {report.FilteredOut:N0}")
Console.WriteLine($"folders pruned : {report.FoldersPruned:N0}")
If report.Warning IsNot Nothing Then Console.WriteLine($"warning : {report.Warning}")
VB.NET — react to specific error codes
Dim report = clsStoreMerger.Merge(sources, "merged.pst") ' overwrite defaults to False
Select Case report.ErrorCode
Case enmErrorCode.Ok
Console.WriteLine("Merged.")
Case enmErrorCode.DestinationExists
Console.WriteLine("Output already exists - pass overwrite:=True to replace it.")
Case enmErrorCode.FileLocked
Console.WriteLine("A file is open in Outlook - close Outlook and retry.")
Case enmErrorCode.DiskFull, enmErrorCode.InsufficientDiskSpace
Console.WriteLine("Not enough disk space.")
Case Else
Console.WriteLine(report.ToString()) ' "code: description"
End Select
Export every message in a store to its own file — .eml (RFC 5322 / MIME) or
.msg (Outlook message, [MS-OXMSG]) — with recipients and attachments, under a folder
tree that mirrors the PST. Pass enmExportFormat.Eml or enmExportFormat.Msg. Add
Imports Bastion.Pst.Export / using Bastion.Pst.Export;.
VB.NET
Dim diag As New clsDiagnostics With {.CreateUserLog = True, .RecoverFromCorruption = True}
Dim rep = clsStoreExporter.ExportToFolder("mail.pst", "C:\out\mail",
enmExportFormat.Eml, diag, overwrite:=True)
If rep.ErrorCode <> enmErrorCode.Ok Then
Console.WriteLine(rep.ToString())
Else
Console.WriteLine($"{rep.FilesWritten:N0} message(s) written, " &
$"{rep.FoldersVisited:N0} folder(s), {rep.Failed:N0} skipped.")
End If
C#
var diag = new clsDiagnostics { CreateUserLog = true, RecoverFromCorruption = true };
var rep = clsStoreExporter.ExportToFolder("mail.pst", @"C:\out\mail",
enmExportFormat.Eml, diag, overwrite: true);
if (rep.ErrorCode != enmErrorCode.Ok)
Console.WriteLine(rep.ToString());
else
Console.WriteLine($"{rep.FilesWritten:N0} message(s) written, " +
$"{rep.FoldersVisited:N0} folder(s), {rep.Failed:N0} skipped.");
To export a single message you already have in hand, use
clsMessageExporter.ExportEml(msg, "message.eml") or
clsMessageExporter.ExportMsg(msg, "message.msg") (each returns a
clsFileOpResult), or clsMessageExporter.ToEml(msg) for the MIME text. To
export the whole store to .msg, pass enmExportFormat.Msg to
ExportToFolder.
The non-throwing open returns a result you check the same way.
VB.NET
Dim opened = clsPersonalStorage.TryOpen("mail.pst")
If opened.ErrorCode <> enmErrorCode.Ok Then
Console.WriteLine(opened.ToString())
Return
End If
Using store = opened.Store
For Each folder In store.RootFolder.SubFolders()
Console.WriteLine($"{folder.DisplayName}: {folder.ContentCount} items")
Next
End Using
C#
var opened = clsPersonalStorage.TryOpen("mail.pst");
if (opened.ErrorCode != enmErrorCode.Ok)
{
Console.WriteLine(opened.ToString());
return;
}
using (var store = opened.Store)
{
foreach (var folder in store.RootFolder.SubFolders())
Console.WriteLine($"{folder.DisplayName}: {folder.ContentCount} items");
}
A task-by-task cookbook covering every feature of the library, each in both
VB.NET and C#. All snippets assume these imports/usings and follow
the return-code pattern — check ErrorCode (0 = Ok); the library never throws into
your code.
VB.NET
Imports Bastion.Pst ' enmErrorCode, result types
Imports Bastion.Pst.Messaging ' clsPersonalStorage, clsFolder, clsMessage, clsMessageQuery, enmItemKind
Imports Bastion.Pst.Convert ' clsStoreMerger, clsStoreConverter, clsStoreSplitter, clsDedup, enmMergeLayout
Imports Bastion.Pst.Validation ' clsPstValidator, clsPstHealth
Imports Bastion.Pst.Repair ' clsPstRepair
Imports Bastion.Pst.Export ' clsStoreBuilder, clsStoreExporter, clsMessageExporter, enmExportFormat
Imports Bastion.Pst.Import ' clsEmlReader, clsMsgReader, clsMboxReader, clsStoreImporter
Imports Bastion.Pst.Authoring ' clsAuthor, clsAuthoringSession, clsItemBuilder
Imports Bastion.Pst.Views ' clsStoreViews, clsItemQuery, clsItemOps, clsContactMerger
Imports Bastion.Pst.Diagnostics ' clsDiagnostics
C#
using Bastion.Pst;
using Bastion.Pst.Messaging;
using Bastion.Pst.Convert;
using Bastion.Pst.Validation;
using Bastion.Pst.Repair;
using Bastion.Pst.Export;
using Bastion.Pst.Import;
using Bastion.Pst.Authoring;
using Bastion.Pst.Views;
using Bastion.Pst.Diagnostics;
clsPstHealth.Check inspects a file the moment you read it and never throws. If it is
damaged, RepairTo rebuilds it into a fresh, validated file, and
SaveSupportReport writes a privacy-safe report (structural metadata only).
VB.NET
Dim health = clsPstHealth.Check("mail.pst")
If health.ErrorCode <> enmErrorCode.Ok Then Console.WriteLine(health.ToString()) : Return
If health.IsCorrupted Then
Console.WriteLine("Damage detected - repairing…")
Dim report = health.SaveSupportReport() ' empty path -> user temp; safe to email
Console.WriteLine("Support report: " & report.Path)
Dim fix = health.RepairTo("mail-repaired.pst")
Console.WriteLine(If(fix.Succeeded, "Repaired -> mail-repaired.pst", fix.ToString()))
Else
Console.WriteLine("Healthy.")
End If
C#
var health = clsPstHealth.Check("mail.pst");
if (health.ErrorCode != enmErrorCode.Ok) { Console.WriteLine(health.ToString()); return; }
if (health.IsCorrupted)
{
Console.WriteLine("Damage detected - repairing…");
var report = health.SaveSupportReport(); // empty path -> user temp; safe to email
Console.WriteLine("Support report: " + report.Path);
var fix = health.RepairTo("mail-repaired.pst");
Console.WriteLine(fix.Succeeded ? "Repaired -> mail-repaired.pst" : fix.ToString());
}
else Console.WriteLine("Healthy.");
For direct control, clsPstRepair.Repair(src, out, diag, overwrite, stripPassword)
returns a clsRepairReport whose Succeeded is true only when the rebuilt file
passes strict validation; it also carries before/after validation reports and
NodesWritten.
clsStoreBuilder.CreateEmpty writes a fresh, Outlook-mountable empty store at the path
you pass, and validates it (reopens it) before returning. The optional storeName sets
the title Outlook shows for the store in its nav pane (omit it to keep the default
“Outlook Data File”).
VB.NET
Dim res = clsStoreBuilder.CreateEmpty("C:\data\new.pst", overwrite:=True, storeName:="My Archive")
If res.ErrorCode = enmErrorCode.Ok Then
Console.WriteLine("Created " & res.Path)
Else
Console.WriteLine(res.ToString()) ' e.g. "7: The file ... already exists ..."
End If
C#
var res = clsStoreBuilder.CreateEmpty(@"C:\data\new.pst", overwrite: true, storeName: "My Archive");
if (res.ErrorCode == enmErrorCode.Ok)
Console.WriteLine("Created " + res.Path);
else
Console.WriteLine(res.ToString()); // e.g. "7: The file ... already exists ..."
See the Quick Start for the full option set. The essentials:
VB.NET
Dim diag As New clsDiagnostics With {.CreateUserLog = True, .RecoverFromCorruption = True}
Dim rep = clsStoreMerger.Merge({"a.pst", "b.pst", "c.pst"}, "merged.pst",
dedup:=New clsDedup(), diag:=diag, overwrite:=True,
layout:=enmMergeLayout.Unified, rootName:="All Mail", pruneEmpty:=True)
Console.WriteLine(If(rep.Succeeded,
$"{rep.TotalNodes:N0} items, {rep.DuplicatesRemoved:N0} duplicates removed", rep.ToString()))
C#
var diag = new clsDiagnostics { CreateUserLog = true, RecoverFromCorruption = true };
var rep = clsStoreMerger.Merge(new[] { "a.pst", "b.pst", "c.pst" }, "merged.pst",
dedup: new clsDedup(), diag: diag, overwrite: true,
layout: enmMergeLayout.Unified, rootName: "All Mail", pruneEmpty: true);
Console.WriteLine(rep.Succeeded
? $"{rep.TotalNodes:N0} items, {rep.DuplicatesRemoved:N0} duplicates removed" : rep.ToString());
VB.NET
Dim vol = clsStoreMerger.MergeToVolumes({"big1.pst", "big2.pst"}, "archive.pst",
capBytes:=10L * 1024 * 1024 * 1024, overwrite:=True)
For Each v In vol.Volumes : Console.WriteLine($"{v.Path}: {v.Messages:N0} msgs") : Next
C#
var vol = clsStoreMerger.MergeToVolumes(new[] { "big1.pst", "big2.pst" }, "archive.pst",
capBytes: 10L * 1024 * 1024 * 1024, overwrite: true);
foreach (var v in vol.Volumes) Console.WriteLine($"{v.Path}: {v.Messages:N0} msgs");
VB.NET
Dim opened = clsPersonalStorage.TryOpen("mail.pst")
If opened.ErrorCode <> enmErrorCode.Ok Then Console.WriteLine(opened.ToString()) : Return
Using store = opened.Store
Walk(store.RootFolder, 0)
End Using
Sub Walk(folder As clsFolder, depth As Integer)
Console.WriteLine(New String(" "c, depth * 2) & $"{folder.DisplayName} ({folder.ContentCount})")
For Each m In folder.Messages()
Console.WriteLine(New String(" "c, depth * 2 + 2) & $"- {m.Subject}")
Next
For Each child In folder.SubFolders() : Walk(child, depth + 1) : Next
End Sub
C#
var opened = clsPersonalStorage.TryOpen("mail.pst");
if (opened.ErrorCode != enmErrorCode.Ok) { Console.WriteLine(opened.ToString()); return; }
using (var store = opened.Store)
Walk(store.RootFolder, 0);
void Walk(clsFolder folder, int depth)
{
Console.WriteLine(new string(' ', depth * 2) + $"{folder.DisplayName} ({folder.ContentCount})");
foreach (var m in folder.Messages())
Console.WriteLine(new string(' ', depth * 2 + 2) + $"- {m.Subject}");
foreach (var child in folder.SubFolders()) Walk(child, depth + 1);
}
VB.NET
Dim m As clsMessage = ... ' from folder.Messages() or a search hit
Console.WriteLine($"Subject : {m.Subject}")
Console.WriteLine($"From : {m.SenderName} <{m.SenderEmail}>")
Console.WriteLine($"Sent : {m.DeliveryTime}")
Console.WriteLine($"Unread : {m.IsUnread}")
For Each r In m.Recipients
Console.WriteLine($" {r.RecipientType}: {r.DisplayName} <{r.SmtpAddress}>")
Next
Console.WriteLine(m.Body)
For Each a In m.Attachments
Console.WriteLine($" attachment: {a.FileName} ({a.Size:N0} bytes, {a.MimeTag})")
Dim bytes = a.GetData()
If bytes IsNot Nothing Then IO.File.WriteAllBytes(a.FileName, bytes) ' save it
Next
C#
clsMessage m = ...; // from folder.Messages() or a search hit
Console.WriteLine($"Subject : {m.Subject}");
Console.WriteLine($"From : {m.SenderName} <{m.SenderEmail}>");
Console.WriteLine($"Sent : {m.DeliveryTime}");
Console.WriteLine($"Unread : {m.IsUnread}");
foreach (var r in m.Recipients)
Console.WriteLine($" {r.RecipientType}: {r.DisplayName} <{r.SmtpAddress}>");
Console.WriteLine(m.Body);
foreach (var a in m.Attachments)
{
Console.WriteLine($" attachment: {a.FileName} ({a.Size:N0} bytes, {a.MimeTag})");
var bytes = a.GetData();
if (bytes != null) System.IO.File.WriteAllBytes(a.FileName, bytes); // save it
}
Every item exposes its Kind; calendar/contact fields are available directly.
VB.NET
For Each m In folder.Messages()
Select Case m.Kind
Case enmItemKind.Appointment
Console.WriteLine($"Appt: {m.Subject} {m.AppointmentStart}-{m.AppointmentEnd} @ {m.Location}")
Case enmItemKind.Contact
Console.WriteLine($"Contact: {m.Subject} {m.ContactEmail}")
Case enmItemKind.Mail
Console.WriteLine($"Mail: {m.Subject}")
End Select
Next
C#
foreach (var m in folder.Messages())
{
switch (m.Kind)
{
case enmItemKind.Appointment:
Console.WriteLine($"Appt: {m.Subject} {m.AppointmentStart}-{m.AppointmentEnd} @ {m.Location}"); break;
case enmItemKind.Contact:
Console.WriteLine($"Contact: {m.Subject} {m.ContactEmail}"); break;
case enmItemKind.Mail:
Console.WriteLine($"Mail: {m.Subject}"); break;
}
}
VB.NET
Using store = clsPersonalStorage.Open("mail.pst")
Dim q = New clsMessageQuery().SubjectContains("invoice").
SenderContains("acme.com").
OfKind(enmItemKind.Mail).
WithAttachments().Unread()
For Each hit In store.Search(q)
Console.WriteLine($"{hit.FolderPath}\{hit.Message.Subject}")
Next
End Using
C#
using (var store = clsPersonalStorage.Open("mail.pst"))
{
var q = new clsMessageQuery().SubjectContains("invoice")
.SenderContains("acme.com")
.OfKind(enmItemKind.Mail)
.WithAttachments().Unread();
foreach (var hit in store.Search(q))
Console.WriteLine($@"{hit.FolderPath}\{hit.Message.Subject}");
}
VB.NET
Dim res = clsStoreConverter.ConvertToUnicode("old-ansi.pst", "unicode.pst",
New clsDiagnostics(), overwrite:=True)
Console.WriteLine(If(res.Succeeded, "Converted.", res.ToString()))
C#
var res = clsStoreConverter.ConvertToUnicode("old-ansi.pst", "unicode.pst",
new clsDiagnostics(), overwrite: true);
Console.WriteLine(res.Succeeded ? "Converted." : res.ToString());
By size, or by any predicate over each message row (here: subject contains a word).
VB.NET
' by size: <= 2 GB parts
Dim r1 = clsStoreSplitter.SplitBySize("big.pst", "C:\out", 2L * 1024 * 1024 * 1024, New clsDiagnostics())
For Each p In r1.Parts : Console.WriteLine($"{p.Path}: {p.MessageCount:N0} msgs") : Next
' by predicate: everything whose subject contains "2024" into one file, the rest into another
Dim pred As Func(Of clsMessage, Boolean) =
Function(m) (If(m.Subject, "")).IndexOf("2024", StringComparison.OrdinalIgnoreCase) >= 0
Dim r2 = clsStoreSplitter.SplitByPredicate("big.pst", "2024.pst", "rest.pst", pred, New clsDiagnostics())
C#
// by size: <= 2 GB parts
var r1 = clsStoreSplitter.SplitBySize("big.pst", @"C:\out", 2L * 1024 * 1024 * 1024, new clsDiagnostics());
foreach (var p in r1.Parts) Console.WriteLine($"{p.Path}: {p.MessageCount:N0} msgs");
// by predicate: subject contains "2024" -> one file, the rest -> another
Func<clsMessage, bool> pred =
m => (m.Subject ?? "").IndexOf("2024", StringComparison.OrdinalIgnoreCase) >= 0;
var r2 = clsStoreSplitter.SplitByPredicate("big.pst", "2024.pst", "rest.pst", pred, new clsDiagnostics());
The bulk exporter mirrors the folder tree; the per-message exporters produce one file. ICS covers appointments, VCF covers contacts (non-matching items are skipped), and MBOX writes one file per folder.
VB.NET
' whole store -> a folder tree of files in any format
Dim rep = clsStoreExporter.ExportToFolder("mail.pst", "C:\out", enmExportFormat.Eml,
New clsDiagnostics(), overwrite:=True)
Console.WriteLine($"{rep.FilesWritten:N0} written, {rep.Failed:N0} skipped")
' …or enmExportFormat.Msg / .Ics / .Vcf / .Mhtml / .Mbox
' a single item you already hold
clsMessageExporter.ExportEml(m, "message.eml") ' RFC 5322 / MIME
clsMessageExporter.ExportMsg(m, "message.msg") ' Outlook [MS-OXMSG]
clsMessageExporter.ExportMhtml(m, "message.mhtml") ' web archive (RFC 2557)
clsItemExporter.ExportIcs(appt, "event.ics") ' appointment -> iCalendar
clsItemExporter.ExportVcf(contact, "card.vcf") ' contact -> vCard 3.0
C#
// whole store -> a folder tree of files in any format
var rep = clsStoreExporter.ExportToFolder("mail.pst", @"C:\out", enmExportFormat.Eml,
new clsDiagnostics(), overwrite: true);
Console.WriteLine($"{rep.FilesWritten:N0} written, {rep.Failed:N0} skipped");
// …or enmExportFormat.Msg / .Ics / .Vcf / .Mhtml / .Mbox
// a single item you already hold
clsMessageExporter.ExportEml(m, "message.eml"); // RFC 5322 / MIME
clsMessageExporter.ExportMsg(m, "message.msg"); // Outlook [MS-OXMSG]
clsMessageExporter.ExportMhtml(m, "message.mhtml"); // web archive (RFC 2557)
clsItemExporter.ExportIcs(appt, "event.ics"); // appointment -> iCalendar
clsItemExporter.ExportVcf(contact, "card.vcf"); // contact -> vCard 3.0
VB.NET
Dim v = clsPstValidator.Validate("mail.pst")
If v Is Nothing Then
Console.WriteLine("ANSI/legacy or unreadable header.")
ElseIf v.IsValid Then
Console.WriteLine($"VALID - {v.PagesChecked:N0} pages, {v.BlocksChecked:N0} blocks.")
Else
Console.WriteLine($"INVALID - {v.ErrorCount} issue(s):")
For Each grp In v.Issues.GroupBy(Function(i) i.Category)
Console.WriteLine($" {grp.Key} x{grp.Count()}: {grp.First().Message}")
Next
End If
C#
var v = clsPstValidator.Validate("mail.pst");
if (v == null)
Console.WriteLine("ANSI/legacy or unreadable header.");
else if (v.IsValid)
Console.WriteLine($"VALID - {v.PagesChecked:N0} pages, {v.BlocksChecked:N0} blocks.");
else
{
Console.WriteLine($"INVALID - {v.ErrorCount} issue(s):");
foreach (var grp in v.Issues.GroupBy(i => i.Category))
Console.WriteLine($" {grp.Key} x{grp.Count()}: {grp.First().Message}");
}
VB.NET
Using store = clsPersonalStorage.Open("mail.pst")
For Each f In AllFolders(store.RootFolder)
Dim declared = f.ContentCount, actual = f.Messages().Count()
If declared <> actual Then Console.WriteLine($"MISMATCH {f.DisplayName}: {declared} vs {actual}")
Next
End Using
C#
using (var store = clsPersonalStorage.Open("mail.pst"))
foreach (var f in AllFolders(store.RootFolder))
{
int declared = f.ContentCount, actual = f.Messages().Count();
if (declared != actual) Console.WriteLine($"MISMATCH {f.DisplayName}: {declared} vs {actual}");
}
One clsDiagnostics drives progress events, a plain-language user log, an automatic
corruption scan on open, and skip-on-error recovery — pass it to any operation.
VB.NET
Dim diag As New clsDiagnostics With {
.CreateUserLog = True, ' plain-language activity log for end users
.RecoverFromCorruption = True, ' skip damaged items instead of aborting
.ScanSourcesOnOpen = True} ' detect corruption as soon as a source is opened
AddHandler diag.WriteProgress, Sub(s, e) Console.Write($"{vbCr}{e.PercentComplete}% ")
AddHandler diag.ErrorOccurred, Sub(s, e) Console.WriteLine($"recovered: {e.Message}")
Dim rep = clsStoreMerger.Merge({"a.pst", "b.pst"}, "merged.pst", diag:=diag, overwrite:=True)
Console.WriteLine(vbCrLf & "activity log: " & diag.UserLogPath)
C#
var diag = new clsDiagnostics {
CreateUserLog = true, // plain-language activity log for end users
RecoverFromCorruption = true, // skip damaged items instead of aborting
ScanSourcesOnOpen = true }; // detect corruption as soon as a source is opened
diag.WriteProgress += (s, e) => Console.Write($"\r{e.PercentComplete}% ");
diag.ErrorOccurred += (s, e) => Console.WriteLine($"recovered: {e.Message}");
var rep = clsStoreMerger.Merge(new[] { "a.pst", "b.pst" }, "merged.pst", diag: diag, overwrite: true);
Console.WriteLine("\nactivity log: " + diag.UserLogPath);
VB.NET
Dim rep = clsStoreMerger.Merge(sources, "merged.pst")
Select Case rep.ErrorCode
Case enmErrorCode.Ok : Console.WriteLine("Done.")
Case enmErrorCode.FileNotFound : Console.WriteLine("A source is missing.")
Case enmErrorCode.FileLocked : Console.WriteLine("Close Outlook and retry.")
Case enmErrorCode.DestinationExists : Console.WriteLine("Pass overwrite:=True.")
Case enmErrorCode.DiskFull,
enmErrorCode.InsufficientDiskSpace : Console.WriteLine("Free up disk space.")
Case enmErrorCode.OutputSizeExceeded,
enmErrorCode.NodeIdCapacityExceeded : Console.WriteLine("Use MergeToVolumes.")
Case Else : Console.WriteLine(rep.ToString() & " / " & rep.Detail)
End Select
C#
var rep = clsStoreMerger.Merge(sources, "merged.pst");
switch (rep.ErrorCode)
{
case enmErrorCode.Ok: Console.WriteLine("Done."); break;
case enmErrorCode.FileNotFound: Console.WriteLine("A source is missing."); break;
case enmErrorCode.FileLocked: Console.WriteLine("Close Outlook and retry."); break;
case enmErrorCode.DestinationExists: Console.WriteLine("Pass overwrite: true."); break;
case enmErrorCode.DiskFull:
case enmErrorCode.InsufficientDiskSpace: Console.WriteLine("Free up disk space."); break;
case enmErrorCode.OutputSizeExceeded:
case enmErrorCode.NodeIdCapacityExceeded: Console.WriteLine("Use MergeToVolumes."); break;
default: Console.WriteLine(rep.ToString() + " / " + rep.Detail); break;
}
The import readers (Imports Bastion.Pst.Import) parse an interchange file into a
neutral clsImportedMessage — headers, addresses, both bodies, and attachments (bytes
decoded). They validate the input and return a result code; they never throw.
VB.NET
Dim res = clsEmlReader.ParseFile("message.eml") ' or clsMsgReader.ParseFile("message.msg")
If res.ErrorCode <> enmErrorCode.Ok Then Console.WriteLine(res.ToString()) : Return
Dim m = res.Message
Console.WriteLine($"{m.Subject} — from {m.FromEmail}, {m.To.Count} to, {m.Attachments.Count} attachment(s)")
For Each a In m.Attachments
IO.File.WriteAllBytes(a.FileName, a.Data) ' save each attachment
Next
C#
var res = clsEmlReader.ParseFile("message.eml"); // or clsMsgReader.ParseFile("message.msg")
if (res.ErrorCode != enmErrorCode.Ok) { Console.WriteLine(res.ToString()); return; }
var m = res.Message;
Console.WriteLine($"{m.Subject} — from {m.FromEmail}, {m.To.Count} to, {m.Attachments.Count} attachment(s)");
foreach (var a in m.Attachments)
System.IO.File.WriteAllBytes(a.FileName, a.Data); // save each attachment
VB.NET
For Each m In clsMboxReader.ReadFile("archive.mbox")
Console.WriteLine($"{m.Date} {m.FromEmail} {m.Subject}")
Next
C#
foreach (var m in clsMboxReader.ReadFile("archive.mbox"))
Console.WriteLine($"{m.Date} {m.FromEmail} {m.Subject}");
clsStoreImporter writes imported messages into a real PST — subject, sender,
recipients, both bodies, internet headers and attachments become native Outlook properties. Pass an
existing store to add to (its content is preserved; the result goes to a new output file), or
Nothing/null to start from a fresh, empty store. The target folder path is
relative to the mail root and is created if missing ("/" separates subfolders).
Folder counts are maintained and the output is validated (reopened) before the call returns.
VB.NET
Imports Bastion.Pst.Import
' A whole MBOX archive into a brand-new PST. storeName sets the title Outlook shows in its nav
' pane (the same idea as the merge tool's rootName); omit it to keep the default "Outlook Data File".
Dim rep = clsStoreImporter.ImportMbox(Nothing, "archive.pst", "archive.mbox", "Imported/2026",
storeName:="2026 Mail Archive")
Console.WriteLine(rep.ToString()) ' "OK: 433 message(s) imported (…)"
' Individual EML/MSG files into an existing store's Inbox (result written to out.pst):
rep = clsStoreImporter.ImportFiles("mail.pst", "out.pst",
IO.Directory.GetFiles("mails", "*.eml"), "Inbox",
overwrite:=True)
If rep.ErrorCode <> enmErrorCode.Ok Then Console.WriteLine(rep.ToString())
For Each line In rep.Failures : Console.WriteLine(line) : Next ' per-file parse problems
C#
using Bastion.Pst.Import;
// A whole MBOX archive into a brand-new PST. storeName titles the store in Outlook's nav pane.
var rep = clsStoreImporter.ImportMbox(null, "archive.pst", "archive.mbox", "Imported/2026",
storeName: "2026 Mail Archive");
Console.WriteLine(rep.ToString()); // "OK: 433 message(s) imported (…)"
// Individual EML/MSG files into an existing store's Inbox (result written to out.pst):
rep = clsStoreImporter.ImportFiles("mail.pst", "out.pst",
System.IO.Directory.GetFiles("mails", "*.eml"), "Inbox",
overwrite: true);
if (rep.ErrorCode != enmErrorCode.Ok) Console.WriteLine(rep.ToString());
foreach (var line in rep.Failures) Console.WriteLine(line); // per-file parse problems
A PST password is not encryption — Outlook stores only a CRC hash of the
password on the message store and prompts on open. clsStorePassword reads that state
(GetStatus), sets or clears it (SetPassword / RemovePassword —
atomic, validated output; pass the same path twice with overwrite:=True to change a file
in place), and computes the hash Outlook would store (ComputeCrc) so you can verify a
user-typed password before opening. The hash is bit-identical to Outlook's (verified against an
independent implementation).
VB.NET
' Is the file password-protected?
Dim status = clsStorePassword.GetStatus("mail.pst")
If status.ErrorCode <> enmErrorCode.Ok Then Console.WriteLine(status.ToString()) : Return
Console.WriteLine(If(status.HasPassword, "Password is set.", "No password."))
' Verify what the user typed without touching the file:
If clsStorePassword.ComputeCrc(userTyped) = status.PasswordCrc Then Console.WriteLine("Correct.")
' Set a password (writes locked.pst; mail.pst is untouched):
Dim setRep = clsStorePassword.SetPassword("mail.pst", "locked.pst", "s3cret!")
Console.WriteLine(If(setRep.Succeeded, "Password set and output validated.", setRep.ToString()))
' Remove it in place:
Dim remRep = clsStorePassword.RemovePassword("locked.pst", "locked.pst", overwrite:=True)
Console.WriteLine(If(remRep.Succeeded, "Opens without a prompt now.", remRep.ToString()))
C#
// Is the file password-protected?
var status = clsStorePassword.GetStatus("mail.pst");
if (status.ErrorCode != enmErrorCode.Ok) { Console.WriteLine(status.ToString()); return; }
Console.WriteLine(status.HasPassword ? "Password is set." : "No password.");
// Verify what the user typed without touching the file:
if (clsStorePassword.ComputeCrc(userTyped) == status.PasswordCrc) Console.WriteLine("Correct.");
// Set a password (writes locked.pst; mail.pst is untouched):
var setRep = clsStorePassword.SetPassword("mail.pst", "locked.pst", "s3cret!");
Console.WriteLine(setRep.Succeeded ? "Password set and output validated." : setRep.ToString());
// Remove it in place:
var remRep = clsStorePassword.RemovePassword("locked.pst", "locked.pst", overwrite: true);
Console.WriteLine(remRep.Succeeded ? "Opens without a prompt now." : remRep.ToString());
The fluent builders in Bastion.Pst.Authoring produce every item type. Hand the
items to clsAuthor.CreateStoreWith (a new store) or AddItems (an existing
one); the target folder is created on demand.
VB.NET
Dim mail = clsItemBuilder.Mail().Subject("Welcome").From("Team", "team@x.test").
[To]("Ada", "ada@x.test").BodyText("Hello!").
AddAttachment("logo.png", pngBytes, "image/png").Build()
Dim contact = clsItemBuilder.Contact().Name("Ada", "Lovelace").Company("Analytical").
Email("ada@x.test").Phone(business:="+44 20 0000").Build()
Dim task = clsItemBuilder.Task().Subject("Ship v1").Status(1).PercentComplete(0.5).
DueDate(New Date(2026, 7, 20)).Build()
Dim note = clsItemBuilder.Note().Text("Remember the milk").Color(3).Build()
Dim journal = clsItemBuilder.Journal().Subject("Call with Ada").JournalType("Phone call").
DurationMinutes(15).Build()
Dim list = clsItemBuilder.DistributionList().Name("Team").
AddMember("Ada", "ada@x.test").AddMember("Alan", "alan@x.test").Build()
Dim r = clsAuthor.CreateStoreWith("new.pst", {mail}, "Inbox", storeName:="My Data")
clsAuthor.AddItems("new.pst", "new.pst", {contact, list}, "Contacts", overwrite:=True)
C#
var appt = clsItemBuilder.Appointment().Subject("Weekly standup")
.StartsAt(new DateTime(2026, 7, 8, 9, 0, 0, DateTimeKind.Utc))
.EndsAt(new DateTime(2026, 7, 8, 9, 30, 0, DateTimeKind.Utc))
.RecurWeekly(days: enmRecurDays.Monday, occurrences: 10) // a recurring series…
.ExcludeOccurrences(new DateTime(2026, 8, 31)) // …skipping a bank holiday
.Build();
var r = clsAuthor.AddItems("new.pst", "new.pst", new[] { appt }, "Calendar", overwrite: true);
The session verbs are RenameFolder, SetFolderClass,
MoveFolder, DeleteFolder, MoveItems, DeleteItems,
EditItems and RemoveAttachments. Creating a folder and adding items are
not session verbs — use clsAuthor.CreateFolder and
clsAuthor.AddItems for those, then batch the edits.
VB.NET
Dim rep = New clsAuthoringSession("data.pst").
RenameFolder("Old", "Archive").
SetFolderClass("Archive", clsFolderClass.Mail).
DeleteItems("Spam", Function(m) m.Subject.Contains("[AD]")).
Commit("out.pst", overwrite:=True) ' one rewrite; ErrorCode 0 == all applied
C#
var rep = new clsAuthoringSession("data.pst")
.RenameFolder("Old", "Archive")
.SetFolderClass("Archive", clsFolderClass.Mail)
.DeleteItems("Spam", m => m.Subject.Contains("[AD]"))
.Commit("out.pst", overwrite: true); // one rewrite; ErrorCode 0 == all applied
A folder is a mail folder ("IPF.Note") until you say otherwise. Its container class
(PidTagContainerClass) is what tells Outlook — and any other client — to render a
calendar grid instead of a message list, so this is the step that turns an authored folder into a
real Calendar or Contacts folder. Use the clsFolderClass constants rather than typing
the strings.
The value is written to both the folder's own property context and its row in the parent's hierarchy table, because different readers consult different ones.
VB.NET
' One folder, one call. The source is never modified — every edit is an atomic rewrite.
Dim rep = clsAuthor.SetFolderClass("data.pst", "out.pst", "Team Calendar",
clsFolderClass.Appointment, overwrite:=True)
Console.WriteLine($"{rep.FoldersReclassified} folder(s) reclassified.")
' Read it back — this is also how you audit an existing store's folder kinds.
Using pst = clsPersonalStorage.Open("out.pst")
For Each top In pst.RootFolder.SubFolders()
For Each f In top.SubFolders()
Console.WriteLine($"[{f.DisplayName}] {f.ContainerClass}")
Next
Next
End Using
C#
// One folder, one call. The source is never modified — every edit is an atomic rewrite.
var rep = clsAuthor.SetFolderClass("data.pst", "out.pst", "Team Calendar",
clsFolderClass.Appointment, overwrite: true);
Console.WriteLine($"{rep.FoldersReclassified} folder(s) reclassified.");
// Read it back — this is also how you audit an existing store's folder kinds.
using var pst = clsPersonalStorage.Open("out.pst");
foreach (var top in pst.RootFolder.SubFolders())
foreach (var f in top.SubFolders())
Console.WriteLine($"[{f.DisplayName}] {f.ContainerClass}");
Every authoring call rewrites the whole store, so type a whole mailbox in one rewrite with a session rather than one rewrite per folder:
VB.NET
Dim rep = New clsAuthoringSession("data.pst").
SetFolderClass("Team Calendar", clsFolderClass.Appointment).
SetFolderClass("Team Contacts", clsFolderClass.Contact).
Commit("mailbox.pst", overwrite:=True)
C#
var rep = new clsAuthoringSession("data.pst")
.SetFolderClass("Team Calendar", clsFolderClass.Appointment)
.SetFolderClass("Team Contacts", clsFolderClass.Contact)
.Commit("mailbox.pst", overwrite: true);
Creating a folder is not a session verb — use clsAuthor.CreateFolder
first, then type the finished tree in one commit.
PowerShell
Set-PstFolderClass C:\mail.pst -Destination C:\out.pst `
-Folder 'Team Calendar' -ContainerClass IPF.Appointment
Get-PstFolder C:\out.pst | Select-Object Name, Class
The classes. clsFolderClass.Mail ("IPF.Note", the default),
Appointment, Contact, Task, StickyNote,
Journal, Birthday, Homepage. An unset class is treated as
mail, so a mail folder does not strictly need one stamped on it.
It identifies a kind, not an identity. The class is inherited by subfolders in
practice — every folder under a Calendar is usually IPF.Appointment too — so two
sibling folders can legitimately share one. Never use it alone to decide that two folders are "the
same folder".
C#
// A new, empty Unicode PST mirroring the source's folder tree (no messages copied).
var rep = clsStoreImporter.CloneStructure("live.pst", "template.pst", overwrite: true);
Console.WriteLine($"{rep.FoldersCreated} folders mirrored.");
OLM is Outlook for Mac's export format (a ZIP of XML). ImportOlm reads the mail and
recreates the OLM folder hierarchy in a new Unicode PST.
C#
var rep = clsStoreImporter.ImportOlm("MacExport.olm", "converted.pst", overwrite: true);
Console.WriteLine($"{rep.MessagesImported} messages into {rep.FoldersCreated} folders.");
// …or read the items into the neutral model without writing a PST:
foreach (var m in clsOlmReader.ReadFile("MacExport.olm"))
Console.WriteLine($"[{m.SourceFolder}] {m.Subject} ({m.Attachments.Count} attachments)");
Every long operation honours a CancellationToken; because writes are atomic, a
cancellation leaves the destination untouched. And because the library only writes Unicode, adding to
or editing a legacy ANSI store would rewrite it as Unicode — which Outlook 2002 and
earlier cannot open — so that upgrade is refused by default and must be authorised explicitly.
C#
// --- cancellation ---
var cts = new CancellationTokenSource();
var diag = new clsDiagnostics { CancellationToken = cts.Token };
// cts.Cancel() from your UI thread… the call returns enmErrorCode.OperationCancelled, nothing written.
var conv = clsStoreConverter.ConvertToUnicode("huge.pst", "out.pst", diag, overwrite: true);
// --- ANSI safety gate ---
if (clsPersonalStorage.IsAnsi("legacy.pst")) // cheap header-only pre-flight
Warn("Legacy ANSI file — adding items upgrades it to Unicode (Outlook 2002- can't open it).");
var g = new clsDiagnostics();
g.AnsiUpgradeRequired += (s, e) => e.Proceed = UserConfirmed; // the hard yes/no
var add = clsAuthor.AddItems("legacy.pst", "out.pst", items, "Inbox", g);
if (add.ErrorCode == enmErrorCode.AnsiUpgradeRequired) { /* declined — source untouched */ }
store.Views is the read model a host application binds to: flat rows, not a message
graph, so a grid binds to Mail(), a card view to Contacts() and a scheduler
to Appointments(). The rows are plain data — no store handles, no lazy reads —
so they are safe to hold and to bind, and a damaged item yields a row with blank fields rather than
taking the view down. Which folders hold contacts, which hold feeds, and which are Outlook's hidden
address-book plumbing is decided here, once, instead of in every host that shows a mailbox.
Note that RSS posts are a section of their own. Outlook files them under a container class that is a variation on “mail”, so anywhere else they are swept in with your messages — in one real archive that was nearly 13,000 posts inflating the mail counts.
VB.NET
Using store = clsPersonalStorage.Open("mail.pst")
For Each m In store.Views.Mail(New clsItemQuery().Take(200))
Console.WriteLine($"{m.FromDisplay} {m.Display} {m.Date:d}" &
If(m.IsUnread, " unread", "") & If(m.HasAttachments, " clip", ""))
Next
For Each c In store.Views.Contacts()
Console.WriteLine($"{c.SortKey} {c.Email} {c.Phone}") ' c.Photo is the card picture
Next
For Each a In store.Views.Appointments()
Console.WriteLine($"{a.Display} {a.StartTime:g}-{a.EndTime:t} @{a.Location}")
Next
For Each t In store.Views.Tasks()
Console.WriteLine($"{t.Display} {t.StatusText} {t.PercentComplete:P0}" &
If(t.IsOverdue, " OVERDUE", ""))
Next
For Each n In store.Views.Notes() : Console.WriteLine(n.Display) : Next
For Each r In store.Views.Rss() : Console.WriteLine($"[{r.FeedName}] {r.Display}") : Next
End Using
C#
using var store = clsPersonalStorage.Open("mail.pst");
foreach (var m in store.Views.Mail(new clsItemQuery().Take(200)))
Console.WriteLine($"{m.FromDisplay} {m.Display} {m.Date:d}"
+ (m.IsUnread ? " unread" : "") + (m.HasAttachments ? " clip" : ""));
foreach (var c in store.Views.Contacts())
Console.WriteLine($"{c.SortKey} {c.Email} {c.Phone}"); // c.Photo is the card picture
foreach (var a in store.Views.Appointments())
Console.WriteLine($"{a.Display} {a.StartTime:g}-{a.EndTime:t} @{a.Location}");
foreach (var t in store.Views.Tasks())
Console.WriteLine($"{t.Display} {t.StatusText} {t.PercentComplete:P0}"
+ (t.IsOverdue ? " OVERDUE" : ""));
foreach (var n in store.Views.Notes()) Console.WriteLine(n.Display);
foreach (var r in store.Views.Rss()) Console.WriteLine($"[{r.FeedName}] {r.Display}");
Every view is a lazy IEnumerable, so LINQ composes over it and nothing is materialised
until you enumerate. Each row carries Nid, FolderPath, FolderNid
and StorePath — enough to act on the item later without re-walking the tree, which
matters when the host has several files open at once.
This is what a mailbox UI actually does, and the two halves have deliberately different shapes. A
list row must be thin — a mail list of 97,803 rows must not carry 97,803 message bodies —
so MailIn reads the folder's contents table and nothing else: a
23,000-message Inbox costs one table read rather than 23,000 message opens. Detail is
the read that happens on selection, and it decides which body to show (a message may hold HTML, RTF,
plain, or several) rather than leaving every caller to guess. Attachment bytes are fetched only when
the user actually saves or opens one.
VB.NET
Using store = clsPersonalStorage.Open("mail.pst")
Dim folder = ... ' the folder the user clicked
' The list: one table read, no message opened.
Dim rows = store.Views.MailIn(folder).ToList()
' The selection: everything needed to display ONE item, in a single pass.
Dim d = store.Views.Detail(folder, rows(0).Nid)
Console.WriteLine($"{d.Subject} - from {d.SenderName} <{d.SenderEmail}> to {d.DisplayTo}")
RenderBody(If(d.HasHtml, d.BodyHtml, d.BodyPlain), asHtml:=d.HasHtml)
' Inline images belong in the body, not the paperclip list.
For Each a In d.Attachments.Where(Function(x) Not x.IsInline)
Dim bytes = store.Views.AttachmentBytes(folder, d.Nid, a.Index) ' on demand
IO.File.WriteAllBytes(IO.Path.Combine(saveDir, a.FileName), bytes)
Next
' The folders that make up a section, so the host can offer them as a list.
For Each sf In store.Views.SectionFolders(enmSection.Calendar)
Console.WriteLine($"{sf.FullPath} [{sf.ItemCount:N0}]")
Next
End Using
C#
using var store = clsPersonalStorage.Open("mail.pst");
var folder = ...; // the folder the user clicked
// The list: one table read, no message opened.
var rows = store.Views.MailIn(folder).ToList();
// The selection: everything needed to display ONE item, in a single pass.
var d = store.Views.Detail(folder, rows[0].Nid);
Console.WriteLine($"{d.Subject} - from {d.SenderName} <{d.SenderEmail}> to {d.DisplayTo}");
RenderBody(d.HasHtml ? d.BodyHtml : d.BodyPlain, asHtml: d.HasHtml);
// Inline images belong in the body, not the paperclip list.
foreach (var a in d.Attachments.Where(x => !x.IsInline))
{
var bytes = store.Views.AttachmentBytes(folder, d.Nid, a.Index); // on demand
File.WriteAllBytes(Path.Combine(saveDir, a.FileName), bytes);
}
// The folders that make up a section, so the host can offer them as a list.
foreach (var sf in store.Views.SectionFolders(enmSection.Calendar))
Console.WriteLine($"{sf.FullPath} [{sf.ItemCount:N0}]");
A LINQ predicate over items can only run once each item has been read, so
“mail from Chris in 2011” expressed as a Where opens all 108,892 messages in a
large archive and discards almost every one. A folder's contents table already holds the columns a
list shows — subject, sender, dates, class, size, flags — so a clsItemQuery is
matched against the table row first, and only rows that survive cause the item to be
opened.
Anything the table cannot answer (a body search, a contact's phone number) still needs the item
and is expressed with Where(…) or BodyContains(…), which run
after opening. Order matters: cheap criteria in the query, expensive ones after.
VB.NET
' Answered entirely by the contents table - nothing is opened to reject a row.
Dim cheap = New clsItemQuery().
SenderContains("chris").
DateFrom(#2011-01-01#).DateBefore(#2012-01-01#).
WithAttachments().
LargerThan(64 * 1024).
Unread().
Take(200)
For Each m In store.Views.Mail(cheap) : Console.WriteLine(m.Display) : Next
' Needs the item - but only for rows the cheap pass already let through.
Dim expensive = New clsItemQuery().SubjectContains("invoice").BodyContains("overdue")
' Confine the walk to part of the tree.
Dim sent = store.Views.Mail(New clsItemQuery().InFolder("Sent Items"))
Dim archives = store.Views.Mail(New clsItemQuery().InFolders(Function(p) p.Contains("Archive")))
C#
// Answered entirely by the contents table - nothing is opened to reject a row.
var cheap = new clsItemQuery()
.SenderContains("chris")
.DateFrom(new DateTime(2011, 1, 1)).DateBefore(new DateTime(2012, 1, 1))
.WithAttachments()
.LargerThan(64 * 1024)
.Unread()
.Take(200);
foreach (var m in store.Views.Mail(cheap)) Console.WriteLine(m.Display);
// Needs the item - but only for rows the cheap pass already let through.
var expensive = new clsItemQuery().SubjectContains("invoice").BodyContains("overdue");
// Confine the walk to part of the tree.
var sent = store.Views.Mail(new clsItemQuery().InFolder("Sent Items"));
var archives = store.Views.Mail(new clsItemQuery().InFolders(p => p.Contains("Archive")));
An archive stores the same person over and over: once per source mailbox, plus a stub every time a
field was added. In a real 9.4 GB archive 852 stored contacts were 307 people
— the same name five times, one entry holding the phone, another the e-mail, another the
company. clsContactMerger folds them together for display: nothing is
written and nothing is thrown away. Fields come from whichever entry has them, values that genuinely
disagree are kept as alternates, and each row records how many entries it came from and which folders
they were in.
Outlook's hidden address-book folders (Recipient Cache, Suggested Contacts, PersonMetadata…) carry a contacts container class but hold plumbing rather than people — in one archive they outnumbered the genuine contacts ten to one — so they are excluded unless you ask for them.
A contacts folder also holds groups (distribution lists), which have none of a
person's fields. A row for one carries IsDistributionList, its
Members (name and address apiece, decoded from the one-off EntryIDs) and a ready-made
MembershipSummary; Combine never folds a group into a person. A host that
ignores the flag draws a card with every line blank, which is exactly what the property model used to
give it.
C#
foreach (var row in store.Views.Contacts())
{
if (row.IsDistributionList)
Console.WriteLine($"{row.Display} ({row.MembershipSummary}): {row.MemberNames}");
else
Console.WriteLine($"{row.Display} {row.Email} {row.Phone}");
}
VB.NET
Using store = clsPersonalStorage.Open("mail.pst")
store.Views.IncludeHiddenFolders = False ' the default: no Recipient Cache et al.
Dim stored = store.Views.Contacts().ToList()
Dim people = clsContactMerger.Combine(stored) ' or CombineByEmail(...) to key on address
Console.WriteLine($"{stored.Count:N0} entries -> {people.Count:N0} cards")
For Each p In people
Console.WriteLine($"{p.Display} {p.Email} {p.Phone}")
If p.MergedFrom > 1 Then
Console.WriteLine($" built from {p.MergedFrom} entries in {String.Join(", ", p.SourceFolders)}")
For Each alt In p.AlternateEmails : Console.WriteLine(" also: " & alt) : Next
End If
If p.Photo IsNot Nothing Then IO.File.WriteAllBytes($"{p.Display}.jpg", p.Photo)
Next
End Using
C#
using var store = clsPersonalStorage.Open("mail.pst");
store.Views.IncludeHiddenFolders = false; // the default: no Recipient Cache et al.
var stored = store.Views.Contacts().ToList();
var people = clsContactMerger.Combine(stored); // or CombineByEmail(...) to key on address
Console.WriteLine($"{stored.Count:N0} entries -> {people.Count:N0} cards");
foreach (var p in people)
{
Console.WriteLine($"{p.Display} {p.Email} {p.Phone}");
if (p.MergedFrom > 1)
{
Console.WriteLine($" built from {p.MergedFrom} entries in {string.Join(", ", p.SourceFolders)}");
foreach (var alt in p.AlternateEmails) Console.WriteLine(" also: " + alt);
}
if (p.Photo != null) File.WriteAllBytes($"{p.Display}.jpg", p.Photo);
}
Subscribe once to clsPstEvents.Default and you are told when a store opens, how far a
folder read has got, when a folder's items have been counted, when a store closes and when something
goes wrong — from anywhere in the library, with no callback threaded through every call. The
read event carries a real total, known the moment the contents table is open, so a
host can show a percentage rather than an indeterminate bar: the point is to reassure the user that a
six-figure folder is loading, not that the application has hung.
Threading: events are raised on whatever thread does the work — often a
background reader — so a UI host must marshal (Control.BeginInvoke) before touching
controls.
VB.NET
AddHandler clsPstEvents.Default.PstOpen,
Sub(s, e) Log($"opened {e.Path}")
AddHandler clsPstEvents.Default.PstFolderSelected,
Sub(s, e) ShowLoading($"{e.Folder}: {e.ItemCount:N0} items")
AddHandler clsPstEvents.Default.PstRead,
Sub(s, e) UiThread(Sub() bar.Value = e.Percent) ' Current / Total / Percent
AddHandler clsPstEvents.Default.PstError,
Sub(s, e) Log($"{e.ErrorCode}: {e.Message} ({e.Path})")
AddHandler clsPstEvents.Default.PstClose,
Sub(s, e) HideLoading()
C#
clsPstEvents.Default.PstOpen += (s, e) => Log($"opened {e.Path}");
clsPstEvents.Default.PstFolderSelected += (s, e) => ShowLoading($"{e.Folder}: {e.ItemCount:N0} items");
clsPstEvents.Default.PstRead += (s, e) => UiThread(() => bar.Value = e.Percent);
clsPstEvents.Default.PstError += (s, e) => Log($"{e.ErrorCode}: {e.Message} ({e.Path})");
clsPstEvents.Default.PstClose += (s, e) => HideLoading();
clsItemOps is the operation layer an application would otherwise write itself, and get
subtly wrong: it knows how to copy a contact as well as a message, how to save a .vcf as
well as a .msg, and it answers “can this store be written to?” in one place.
Two rules hold throughout:
VB.NET
' Ask the library why, rather than inventing your own wording for the user.
If Not clsItemOps.IsWritable(target) Then MessageBox.Show(clsItemOps.WhyNotWritable(target)) : Return
' Save to disk in a format other applications open: mail/.msg, contacts/.vcf, appointments/.ics,
' notes/.txt - or force one format for the lot. Cancellable, and it reports progress.
Dim rep = clsItemOps.SaveItems(storePath, folderPath, nids, "C:\out",
enmSaveFormat.Native, cts.Token, Sub(n) bar.Value = n)
Console.WriteLine(If(rep.Ok, $"saved {rep.Saved} file(s)", rep.Error))
' Copy into another data file. Every kind copies with full fidelity - recipients, attachments,
' MAPI properties - because each item goes out as a native .msg and back in.
Dim c = clsItemOps.CopyItems(sourcePath, sourceFolder, nids, targetPath, targetOut, "Inbox", diag)
' Move and delete are the same shape.
Dim m = clsItemOps.MoveItems(storePath, outPath, "Inbox", "Archive", nids, diag)
Dim d = clsItemOps.DeleteItems(storePath, outPath, "Inbox", nids, diag)
' Import .msg .eml .ics .vcf .mbox into a folder.
Dim i = clsItemOps.ImportFiles(storePath, outPath, "Inbox", files, diag)
C#
// Ask the library why, rather than inventing your own wording for the user.
if (!clsItemOps.IsWritable(target)) { MessageBox.Show(clsItemOps.WhyNotWritable(target)); return; }
// Save to disk in a format other applications open: mail/.msg, contacts/.vcf, appointments/.ics,
// notes/.txt - or force one format for the lot. Cancellable, and it reports progress.
var rep = clsItemOps.SaveItems(storePath, folderPath, nids, @"C:\out",
enmSaveFormat.Native, cts.Token, n => bar.Value = n);
Console.WriteLine(rep.Ok ? $"saved {rep.Saved} file(s)" : rep.Error);
// Copy into another data file. Every kind copies with full fidelity - recipients, attachments,
// MAPI properties - because each item goes out as a native .msg and back in.
var c = clsItemOps.CopyItems(sourcePath, sourceFolder, nids, targetPath, targetOut, "Inbox", diag);
// Move and delete are the same shape.
var m = clsItemOps.MoveItems(storePath, outPath, "Inbox", "Archive", nids, diag);
var d = clsItemOps.DeleteItems(storePath, outPath, "Inbox", nids, diag);
// Import .msg .eml .ics .vcf .mbox into a folder.
var i = clsItemOps.ImportFiles(storePath, outPath, "Inbox", files, diag);
Every write is an atomic rewrite through the authoring pipeline: the new file is built, verified and swapped in, so an interrupted operation leaves the original untouched.
The builders cover the parts of a mailbox that are easy to get wrong by hand: a meeting's organiser and attendee list, a recurring series with one instance moved and another cancelled, a contact photo, read state and importance, and RSS posts filed where Outlook files them. Exception dates must land on an occurrence of the series — the library refuses one that does not, rather than writing a calendar Outlook would silently misread.
Then fill the store in one pass. Because every write is a whole-file rewrite, an import call per folder is O(S·k) I/O where one call that groups by folder is O(S) — the difference between minutes and hours on a large store.
VB.NET
Dim items As New List(Of clsImportedMessage)()
Dim mail = clsItemBuilder.Mail().
Subject("Q3 numbers").From("Dana Ruiz", "dana@example.com").To("You", "you@example.com").
BodyHtml("<p>Attached.</p>").SentOn(Date.Now.AddDays(-2)).
Read(False).Importance(2).Build() ' arrives unread, flagged high
mail.SourceFolder = "Inbox" : items.Add(mail)
Dim first = NextTuesday(Date.Today).AddHours(10)
Dim meeting = clsItemBuilder.Appointment().
Subject("Design review").Location("Room 2").
StartsAt(first).EndsAt(first.AddHours(1)).
Organiser("Dana Ruiz", "dana@example.com").
Attendee("You", "you@example.com").OptionalAttendee("Sam Okafor", "sam@example.com").
Reminder(15).
RecurWeekly(enmRecurDays.Tuesday, occurrences:=8).
ModifyOccurrence(first.AddDays(7), newStart:=first.AddDays(7).AddHours(4)). ' moved to 14:00
ExcludeOccurrences(first.AddDays(14)). ' cancelled
Build()
meeting.SourceFolder = "Calendar" : items.Add(meeting)
Dim contact = clsItemBuilder.Contact().
Name("Sam", "Okafor").Company("Example Ltd").JobTitle("Architect").
Email("sam@example.com").Phone(business:="+44 20 7946 0000", mobile:="+44 7700 900000").
Photo(IO.File.ReadAllBytes("sam.jpg")).Build() ' the picture on the card
contact.SourceFolder = "Contacts" : items.Add(contact)
Dim post = clsItemBuilder.Rss().
Subject("Release 3.1 is out").Channel("Example Blog", "https://example.com/feed").
Article("https://example.com/3-1").PostedOn(Date.Now.AddDays(-1)).Read(False).Build()
post.SourceFolder = "RSS Feeds\Example Blog" : items.Add(post)
' ONE pass over the store for every folder; extraFolders are created empty, ready for the user.
Dim rep = clsStoreImporter.ImportMessagesByFolder(Nothing, "new-mailbox.pst", items,
defaultFolder:="Inbox", extraFolders:={"Tasks", "Notes"},
diag:=New clsDiagnostics(), overwrite:=True, storeName:="Authored Mailbox")
C#
var items = new List<clsImportedMessage>();
var mail = clsItemBuilder.Mail()
.Subject("Q3 numbers").From("Dana Ruiz", "dana@example.com").To("You", "you@example.com")
.BodyHtml("<p>Attached.</p>").SentOn(DateTime.Now.AddDays(-2))
.Read(false).Importance(2).Build(); // arrives unread, flagged high
mail.SourceFolder = "Inbox"; items.Add(mail);
var first = NextTuesday(DateTime.Today).AddHours(10);
var meeting = clsItemBuilder.Appointment()
.Subject("Design review").Location("Room 2")
.StartsAt(first).EndsAt(first.AddHours(1))
.Organiser("Dana Ruiz", "dana@example.com")
.Attendee("You", "you@example.com").OptionalAttendee("Sam Okafor", "sam@example.com")
.Reminder(15)
.RecurWeekly(enmRecurDays.Tuesday, occurrences: 8)
.ModifyOccurrence(first.AddDays(7), newStart: first.AddDays(7).AddHours(4)) // moved to 14:00
.ExcludeOccurrences(first.AddDays(14)) // cancelled
.Build();
meeting.SourceFolder = "Calendar"; items.Add(meeting);
var contact = clsItemBuilder.Contact()
.Name("Sam", "Okafor").Company("Example Ltd").JobTitle("Architect")
.Email("sam@example.com").Phone(business: "+44 20 7946 0000", mobile: "+44 7700 900000")
.Photo(File.ReadAllBytes("sam.jpg")).Build(); // the picture on the card
contact.SourceFolder = "Contacts"; items.Add(contact);
var post = clsItemBuilder.Rss()
.Subject("Release 3.1 is out").Channel("Example Blog", "https://example.com/feed")
.Article("https://example.com/3-1").PostedOn(DateTime.Now.AddDays(-1)).Read(false).Build();
post.SourceFolder = @"RSS Feeds\Example Blog"; items.Add(post);
// ONE pass over the store for every folder; extraFolders are created empty, ready for the user.
var rep = clsStoreImporter.ImportMessagesByFolder(null, "new-mailbox.pst", items,
defaultFolder: "Inbox", extraFolders: new[] { "Tasks", "Notes" },
diag: new clsDiagnostics(), overwrite: true, storeName: "Authored Mailbox");
Bastion PST SDK is an ordinary .NET assembly, so PowerShell can drive all of it with Add-Type
and no COM, no Outlook and no MAPI — which makes it a practical tool for the jobs administrators
actually have: auditing a directory full of archives, pulling a mailbox out to files, merging a decade
of PSTs, or checking a hundred files for corruption overnight. The distribution ships a ready-made
module, samples/PowerShell/Bastion.Pst.psm1, with fourteen worked tasks beside it in
Examples.ps1.
POWERSHELL
Import-Module .\Bastion.Pst.psm1
# What is in this file?
Get-PstInfo C:\mail.pst
# The ten fullest folders.
Get-PstFolder C:\mail.pst | Sort-Object Items -Descending | Select-Object -First 10
# Big unread mail with attachments, as a spreadsheet.
Get-PstMail C:\mail.pst -Unread -WithAttachments -Since '2024-01-01' -LargerThanKB 256 |
Export-Csv unread.csv -NoTypeInformation
# The address book, one card per person, photographs written out beside it.
Get-PstContact C:\mail.pst -PhotoDir C:\photos | Export-Csv contacts.csv -NoTypeInformation
# Audit a whole directory of archives; then merge them.
Get-ChildItem C:\Archives\*.pst | Test-PstHealth | Where-Object Corrupted
Get-ChildItem C:\Archives\*.pst | Merge-Pst -Destination C:\merged.pst -Layout Unified
# Rebuild a damaged file into a new copy (the original is never touched).
Repair-Pst C:\damaged.pst -Destination C:\repaired.pst
# Everything out as .msg, with the library's own progress bar.
Register-PstProgress
Export-PstItem C:\mail.pst -Destination C:\out -Format Msg
Unregister-PstProgress
Three things catch everybody out once.
lib\net48; PowerShell 7+ runs on
modern .NET and must load lib\net8.0. Loading the wrong one fails with a
“Bad IL format” that looks like a corrupt download and is not.$null where the .NET default is Nothing.Where-Object. A
Where-Object runs after each item has been read, so filtering a
hundred-thousand-message archive that way opens a hundred thousand messages. The module's
parameters map onto clsItemQuery, which is matched against the folder's contents
table before an item is opened (see example 27).POWERSHELL
# Loading, and calling the raw API without the module.
$tfm = if ($PSVersionTable.PSEdition -eq 'Core') { 'net8.0' } else { 'net48' }
Add-Type -Path "C:\BastionPstSdk\lib\$tfm\Bastion.Pst.dll"
$diag = New-Object Bastion.Pst.Diagnostics.clsDiagnostics
$diag.RecoverFromCorruption = $true
# Merge(sources, out, dedup, keepKinds, diag, overwrite, extraDrops, maxBytes, layout, rootName)
# - every argument supplied, because PowerShell binds positionally.
$sources = New-Object 'System.Collections.Generic.List[string]'
Get-ChildItem C:\Archives\*.pst | ForEach-Object { $sources.Add($_.FullName) }
$rep = [Bastion.Pst.Convert.clsStoreMerger]::Merge(
$sources, 'C:\merged.pst',
(New-Object Bastion.Pst.Convert.clsDedup), $null, $diag, $true, $null, 0,
[Bastion.Pst.Convert.enmMergeLayout]::Unified, $null)
# The library never throws into your script - check the code.
if ($rep.ErrorCode -ne 0) { Write-Warning $rep.ErrorDescription }
else { "merged {0:N0} item(s), {1:N0} duplicate(s) removed" -f $rep.MessagesMerged, $rep.DuplicatesRemoved }
# Progress and errors from anywhere in the library, in one subscription.
$hub = [Bastion.Pst.clsPstEvents]::Default
Register-ObjectEvent -InputObject $hub -EventName PstRead -SourceIdentifier Bastion.Pst.Read -Action {
$e = $Event.SourceEventArgs
Write-Progress -Activity "Reading $($e.Folder)" -Status "$($e.Current) of $($e.Total)" -PercentComplete $e.Percent
} | Out-Null
The distribution ships ready-to-open sample projects under the samples/ folder. Each
references the library from dist/lib and can be opened directly in Visual Studio. Build
one, point it at a PST, and step through the API.
| Project | Language / target | What it does |
|---|---|---|
samples/FeatureTourCSharp |
C# · SDK-style · .NET 8 | The comprehensive C# tour. A single guided walkthrough that exercises the whole
library in sixteen labelled sections: open & inspect, walk folders & read messages, typed
items (AsItem()), fluent search, export, merge & split, health & repair,
password, authoring (clsAuthor / clsAuthoringSession) and import
— then the host-application half: the bindable section views, listing
one folder and reading one item (MailIn /
Detail / AttachmentBytes), filtering before an item is
opened (clsItemQuery), progress events
(clsPstEvents), item operations (clsItemOps save /
copy / move / delete / import) and building a whole mailbox — meeting attendees,
recurrence exceptions, contact photos, feed posts — in one pass. Run it as
FeatureTourCSharp <source.pst> <workDir> and read the console top to
bottom to see every major API called and result-checked. |
samples/FeatureTourVB |
VB.NET · SDK-style · .NET 8 | The comprehensive VB.NET tour. The same sixteen-section walkthrough as the C# tour, written idiomatically in VB.NET — the fastest way to see the full feature set from VB. |
samples/WinFormsBastion.Pst.Samples.WinForms |
VB.NET · classic project · .NET Framework 4.7.2 | The flagship GUI sample — a tabbed WinForms app exercising the whole library: Batch (merge / convert / split / export to EML / export to MSG), Search (fluent query), and Verify / Validate / Repair. Streams progress and a plain-language activity log into an on-form console. Deliberately written in VB 11 syntax so it opens in every Visual Studio from 2012 onward. |
samples/PowerShellBastion.Pst.psm1 + Examples.ps1 |
PowerShell · Windows PowerShell 5.1 and PowerShell 7+ | The whole library as PowerShell functions. Bastion.Pst.psm1 wraps the
calls a script actually wants — Get-PstInfo, Get-PstFolder,
Get-PstMail, Get-PstContact, Get-PstAppointment,
Export-PstItem, Merge-Pst, Test-PstHealth,
Repair-Pst, Import-PstFile, Register-PstProgress — as
cmdlet-shaped functions that emit objects, so the output pipes into Where-Object,
Export-Csv and Out-GridView. Examples.ps1 is fourteen
worked tasks: audit a directory of archives, chart mail volume by year, dump the address book
with photos, merge a decade of PSTs, repair a damaged file. See
the PowerShell example for the three things that catch everyone out. |
samples/ConsoleExportCSharp |
C# · SDK-style · .NET 8 | Minimal command-line exporter. Takes a PST, an output folder and a format
(eml | msg | ics | vcf | mhtml | mbox) and writes every message out via
clsStoreExporter.ExportToFolder, showing the result-code pattern and a progress
counter. The shortest end-to-end example of the export API in C#. |
samples/ConsoleImportVB |
VB.NET · SDK-style · .NET 8 | Minimal command-line importer. Reads an .eml, .msg or
.mbox file into the neutral clsImportedMessage model
(clsEmlReader / clsMsgReader / clsMboxReader) and prints
subject, addresses, body sizes and attachments — or, given an output path, writes the
messages into a new PST via clsStoreImporter. Shows how to
consume the import API and check result codes. |
Each sample writes a log file for every run, whether or not anything goes wrong, to
%TEMP%\BastionPstSdk-Samples\. When something fails you get one plain sentence on screen
and the offer to open the log; the exception type and stack go to the file, not the console. The
WinForms sample also has a View Log File button.
The samples are written the way we suggest you write your own code against the library: every
call is checked by ErrorCode rather than wrapped in a catch, because the
library reports failure in its result and does not throw into your application. The
SampleLog file in each sample is self-contained — copy it into your own project if
it is useful.
One limit worth knowing: a managed try/catch only sees managed exceptions. If a
process is killed by a native fault (heap corruption, an access violation, a stack overflow) no
handler runs at all, because Windows terminates it without unwinding. The log still helps, because
the last line written tells you how far it got — which is why every line is flushed as it
happens rather than buffered.
samples/WinForms/Sample.sln (VS 2012+) and press F5.
It references dist/lib/net472/Bastion.Pst.dll. To target a different runtime, change
the reference's HintPath (see the FAQ).dotnet build (or open in VS 2022) — they reference
dist/lib/net8.0/Bastion.Pst.dll. Run them with no arguments to see usage.Import-Module .\Bastion.Pst.psm1 loads
the assembly that matches your PowerShell edition, then
.\Examples.ps1 -Pst C:\mail.pst runs the worked tasks.All samples follow the house rule demonstrated throughout this document: check each call's
ErrorCode (0 = Ok) rather than catching exceptions — the library never throws into your
application.
The distribution ships the library built for every current .NET. Reference the one that matches your project (the API is identical across all of them):
| Folder | Target | Use from |
|---|---|---|
lib/net46 … lib/net481 | .NET Framework 4.6, 4.6.1, 4.6.2, 4.7, 4.7.1, 4.7.2, 4.8, 4.8.1 | .NET Framework apps; Visual Studio 2012–2022 |
lib/netcoreapp2.0 … lib/netcoreapp3.1 | .NET Core 2.0, 2.1, 3.0, 3.1 | .NET Core apps (VS 2017 15.3+) |
lib/net5.0 … lib/net10.0 | .NET 5, 6, 7, 8 (LTS), 9, 10 (LTS) | Modern .NET apps (VS 2022 17.8+ for net8.0, 17.12+ for net9.0, 17.14+ for net10.0) |
Eighteen builds, one per framework — there is no netstandard build.
Pick the folder matching your target exactly. A WinForms app on net8.0-windows uses
lib/net8.0: the library is portable and needs no Windows-specific build.
The shipped WinForms sample (samples/WinForms) references lib/net472 so it
opens in Visual Studio 2012 — the oldest supported IDE. To point the same sample at a
different build, change one line and (for modern targets) the target framework:
To use a different .NET Framework build — edit the
<HintPath> in the .vbproj. The Include is the
assembly name, Bastion.Pst, not the product name:
<Reference Include="Bastion.Pst">
<HintPath>..\..\lib\net462\Bastion.Pst.dll</HintPath>
</Reference>
Off .NET Framework, add the companion package:
System.Text.Encoding.CodePages (needed for correct ANSI→Unicode transcoding; it is
built into .NET Framework, so you only need it on .NET Core / .NET 5+).
To use net8.0 — convert the project to the SDK style and set the
framework and reference:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
<ItemGroup>
<Reference Include="Bastion.Pst">
<HintPath>..\..\lib\net8.0\Bastion.Pst.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
The same pattern works for net48, net9.0 or net10.0 —
point the <HintPath> at that folder (and, for the modern runtimes, set the matching
<TargetFramework>, e.g. net10.0-windows). The library API is identical
across every build, so the sample source does not change — only the reference and target
framework do. (The shipped sample deliberately uses VB 11 syntax so it also compiles on the
oldest toolchains; newer language features are fine on newer targets.)
Almost always this is Outlook’s view/filter state, not missing data. The folder count comes from the PST’s stored count; Outlook then applies the active View (filter / group / sort) to decide what to show. A stray filter can hide a full folder.
To prove the data is present independent of the view, call
clsPstHealth.Check — it compares each folder’s stored count (and the
folder-tree’s cached count) against the real messages. Zero mismatches means every message is
there and the display is the only variable.
By design a merge removes provably-empty messages (no subject, no body in any
form, no attachments — typically old spam remnants that open to nothing) and
prunes empty folders. Every removal is itemised in the user log when
CreateUserLog is on, with folder, source, sender and date. To keep everything verbatim,
set dropBlankMessages:=False and pruneEmpty:=False (see
Quick Start example 4).
Every operation returns an object derived from clsOperationResult. Check
ErrorCode (an enmErrorCode; 0 = Ok). For display use
ErrorDescription (human text) or ToString() ("code: description");
Detail carries the full technical text for a support ticket, and Succeeded
is a boolean shortcut. The library never throws into your application, so you never
need a try/catch for control flow.
| # | Name | Typical cause / fix |
|---|---|---|
| 0 | Ok | Success. |
| 1 | UnknownError | Unclassified — read Detail. |
| 2 | FileNotFound | A source/destination path does not exist. |
| 3 | FileLocked | Usually the PST is mounted in Outlook — close Outlook. |
| 4 | DiskFull | Destination disk ran out of space. |
| 5 | AccessDenied | Permissions / read-only path. |
| 6 | InvalidPstFormat | Not a readable PST/OST; try Repair. |
| 7 | DestinationExists | Pass overwrite:=True. |
| 8 | OutputSizeExceeded | Output hit the size guard; use MergeToVolumes. |
| 9 | InvalidArgument | A parameter is out of range (message says which). |
| 10 | InvalidRootName | RootName empty after cleaning, or > 200 code units. |
| 11 | NodeIdCapacityExceeded | Too many items for one file; merge into volumes. |
| 12 | InsufficientDiskSpace | Not enough free space for the estimated output. |
The authoritative list is the enmErrorCode reference below.
Yes — pass rootName. Any printable Unicode is accepted (CJK, emoji, right-to-left
scripts, symbols such as \ / : * ? < > | & %). Control characters are stripped
and whitespace trimmed. The length limit is 200 UTF-16 code units (a safety cap, not
a PST limit): 200 CJK characters fit; emoji count as two units each. Defaults are
“Stacked Merge” / “Unified Merge” for multi-source merges, and the
original store name is preserved for a single-source rewrite.
The PST is almost certainly mounted in Outlook, which holds it open with no read sharing. Close Outlook (or remove the store from the profile) and retry. The library pre-checks this before writing and reports it as code 3 with a message that names the likely cause.
Yes — it is an ordinary managed assembly, so Add-Type -Path Bastion.Pst.dll is all
it takes; no COM, no Outlook, no MAPI. samples/PowerShell ships a module wrapping the
whole library as cmdlet-shaped functions (Get-PstInfo, Get-PstMail,
Merge-Pst, Test-PstHealth, Repair-Pst…) that emit objects,
so results pipe into Export-Csv and the rest. Load lib\net48 from
Windows PowerShell 5.1 and lib\net8.0 from PowerShell 7+; the wrong one
fails with a misleading “Bad IL format”. And remember PowerShell cannot skip .NET
optional arguments — pass the whole list, with $null for the defaults. See
example 32.
On .NET Core / .NET 5+ you must register the code-page provider once at startup so ANSI PSTs
transcode correctly — add the System.Text.Encoding.CodePages package and call
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance). This is built into .NET
Framework, so the net46–net481 consumers need nothing.
Yes, for Outlook 2003 and every later version (2007, 2010, 2013, 2016, 2019, 2021, 2024 and Microsoft 365). Bastion PST SDK always writes the modern Unicode PST format. It will not open in Outlook 2002/XP or earlier, which only support the legacy ANSI PST format — there is no ANSI-write mode. Bastion PST SDK can, however, read those old ANSI files and convert them to Unicode. See the Outlook compatibility table in the Introduction.
Because of this, adding to or editing an existing ANSI store would rewrite it as Unicode
and make it unopenable in Outlook 2002 and earlier. To protect users on legacy Outlook, Bastion PST SDK
refuses that upgrade by default (the operation returns
enmErrorCode.AnsiUpgradeRequired and writes nothing). Authorise it explicitly with
clsDiagnostics.AllowAnsiToUnicodeUpgrade = True or by handling the
AnsiUpgradeRequired event. Detect the format up front with the cheap, header-only
clsPersonalStorage.IsAnsi / PeekFormat.
clsPstHealth.Check compares stored vs actual
message counts for every folder, at both the folder-property and folder-tree-cache levels.CreateUserLog, every duplicate removed,
blank dropped and folder pruned is itemised, so input-vs-output always reconciles.Use clsStoreExporter.ExportToFolder(pstPath, outDir, enmExportFormat.Eml, diag) to write
every message to its own .eml file under a folder tree that mirrors the store (attachments
included, RFC 5322 / MIME). For a single message you hold, call
clsMessageExporter.ExportEml(msg, path) or ExportMsg(msg, path). Six formats
ship: EML (RFC 5322 / MIME), MSG (Outlook [MS-OXMSG]),
ICS, VCF, MHTML and MBOX. If you
are saving a user's selection rather than a whole store, use
clsItemOps.SaveItems instead: it picks the right format per item kind (mail .msg,
contact .vcf, appointment .ics, note .txt), is cancellable and reports progress. The
command-line Export tool and the WinForms sample's “Export messages to EML/MSG
files” batch operations both drive the store exporter.
Bind to store.Views rather than writing the projection yourself: Mail(),
Contacts(), Appointments(), Tasks(), Notes() and
Rss() return flat, bindable rows, and MailIn(folder) is what a message list
binds to when the user clicks a folder. Read one item with Views.Detail(folder, nid) when
it is selected, and fetch attachment bytes with Views.AttachmentBytes only when the user
saves or opens one. Editing verbs — save, copy, move, delete, import — are on
clsItemOps. See examples 25–31.
Two causes, both avoidable.
Where over items
can only run after each item has been read. Express the filter as a clsItemQuery
instead: subject, sender, dates, size, unread and has-attachments are all answered by the
folder's contents table, so a rejected row never causes a message to be opened. Keep only what
the table cannot answer (body text, contact fields) in Where /
BodyContains.clsPstEvents.Default.PstRead: it carries Current, Total and
Percent, with the total known from the moment the contents table is open, so you can
show a real percentage rather than an indeterminate bar. Events are raised on the working thread
— marshal to the UI thread before touching controls.No. Diagnostic logs and support reports contain structural metadata only — never message content, subjects, addresses or attachments. They are safe for a user to email to your support (or to support@bastionsoftwaresolutions.com).
Delete any bin\, obj\, *.suo next to the project and re-open,
and start Visual Studio from the Start menu (not from a Git Bash / MSYS shell, whose environment
variables can redirect MSBuild imports). The shipped project has no such reference.
Public types and members only. Grouped by namespace below.
Bastion.Pst
Result of a file-producing call (e.g. SaveSupportReport): the path plus the error contract.
| Member | Type | Summary |
|---|---|---|
Path | String | Full path of the file written (Nothing on failure). |
Bastion.Pst
Result for operations with no extra payload (e.g. ConvertToUnicode).
Bastion.Pst
Base of every operation result: error code + descriptions + ToString "code: description".
| Member | Type | Summary |
|---|---|---|
Detail | String | Full technical detail (exception text incl. stack trace) — for logs/support, may be empty. |
ErrorCode | enmErrorCode | 0 (Ok) = success; otherwise the failure class. |
ErrorDescription | String | One-line, human-readable reason. "OK" on success. |
Succeeded | Boolean | True when ErrorCode = Ok. |
| Member | Type | Summary |
|---|---|---|
ToString() | String | "0: OK" or e.g. "3: The file 'x.pst' is locked by another process …". |
Bastion.Pst
| Member | Type | Summary |
|---|---|---|
Path | String |
Bastion.Pst
| Member | Type | Summary |
|---|---|---|
ErrorCode | enmErrorCode | |
Message | String | |
Path | String |
Bastion.Pst
Subscribe to Default to observe PST activity across the whole library.
| Member | Type | Summary |
|---|---|---|
Default | clsPstEvents | The single, process-wide event source. Accessible from any app. |
Bastion.Pst
| Member | Type | Summary |
|---|---|---|
Folder | String | |
ItemCount | Int64 | |
Path | String |
Bastion.Pst
| Member | Type | Summary |
|---|---|---|
Path | String |
Bastion.Pst
| Member | Type | Summary |
|---|---|---|
Current | Int64 | |
Folder | String | |
Path | String | |
Percent | Int32 | Percent read, 0..100; 0 while the total is not yet known. |
Total | Int64 |
Bastion.Pst
Failure classes returned by every Bastion PST SDK operation. 0 = success.
| Member | Value |
|---|---|
Ok | 0 |
UnknownError | 1 |
FileNotFound | 2 |
FileLocked | 3 |
DiskFull | 4 |
AccessDenied | 5 |
InvalidPstFormat | 6 |
DestinationExists | 7 |
OutputSizeExceeded | 8 |
InvalidArgument | 9 |
InvalidRootName | 10 |
NodeIdCapacityExceeded | 11 |
InsufficientDiskSpace | 12 |
AnsiUpgradeRequired | 13 |
OperationCancelled | 14 |
Bastion.Pst.Authoring
| Member | Type | Summary |
|---|---|---|
AllDay(Boolean) | clsAppointmentBuilder | |
Attendee(String, String) | clsAppointmentBuilder | Add a required attendee. The attendee list is written both as recipients and as the [MS-OXOCAL] attendee-string properties, so it reads back through clsAppointmentView.Attendees. |
BodyText(String) | clsAppointmentBuilder | |
BusyStatus(Int32) | clsAppointmentBuilder | 0 free, 1 tentative, 2 busy, 3 out of office. |
EndsAt(DateTime) | clsAppointmentBuilder | |
ExcludeOccurrences(DateTime[]) | clsAppointmentBuilder | Delete specific occurrence dates from the series (e.g. skip a holiday). Call after a Recur… method; the day is what matters. |
Location(String) | clsAppointmentBuilder | |
ModifyOccurrence(DateTime, Nullable(Of DateTime), Nullable(Of DateTime), String, String, Nullable(Of Int32)) | clsAppointmentBuilder | Override ONE occurrence of the series — move it and/or change its subject, location or busy status for that instance only ([MS-OXOCAL] exception). Call after a Recur… method. originalDate must be a date the pattern generates (and not an excluded one); a moved occurrence must stay on a day no other occurrence lands on. |
OptionalAttendee(String, String) | clsAppointmentBuilder | Add an OPTIONAL attendee (the meeting's Cc line). |
Organiser(String, String) | clsAppointmentBuilder | The meeting organiser (defaults to the store owner when unset). |
RecurDaily(Int32, Int32, Nullable(Of DateTime)) | clsAppointmentBuilder | Make this a daily series: every interval days, ending after occurrences (0 = use until, or never if both unset). |
RecurMonthly(Int32, Int32, Int32, Nullable(Of DateTime)) | clsAppointmentBuilder | Make this a monthly series on day dayOfMonth (1–31; 31 = last), every interval months. |
RecurWeekly(enmRecurDays, Int32, Int32, Nullable(Of DateTime)) | clsAppointmentBuilder | Make this a weekly series on days, every interval weeks (0 occurrences = use until, or never if both unset). |
Reminder(Int32) | clsAppointmentBuilder | |
StartsAt(DateTime) | clsAppointmentBuilder | |
Subject(String) | clsAppointmentBuilder |
Bastion.Pst.Authoring
Creates and populates PST stores without Outlook: build typed items with the clsItemBuilder factories, then add them to a new or existing store. Every write is an atomic rewrite validated by reopening — the store is never mutated in place.
| Member | Type | Summary |
|---|---|---|
AddItems(String, String, IEnumerable(Of clsImportedMessage), String, clsDiagnostics, Boolean, String) | clsImportReport | Add typed items to folderPath of an existing store, writing the result to outPath (may equal storePath). Missing folders in the path are created with the container class matching the item kinds. |
AddItemsByFolder(String, String, IEnumerable(Of clsImportedMessage), String, IEnumerable(Of String), clsDiagnostics, Boolean, String) | clsImportReport | Add typed items spread across MANY folders in one atomic rewrite: each item is filed by its clsImportedMessage.SourceFolder (or defaultFolder when unset), and extraFolders names folders to create empty. Prefer this over a loop of AddItems calls when populating several folders — every AddItems call rewrites the whole store, so a loop costs O(store size × folder count). |
CreateFolder(String, String, String, clsDiagnostics, Boolean) | clsImportReport | Create an (empty) folder at folderPath in storePath, writing to outPath. Implemented as an add of zero items to the path, which the pipeline satisfies by creating the folder chain. |
CreateStore(String, String, Boolean) | clsFileOpResult | Create a new empty Unicode PST at path. |
CreateStoreWith(String, IEnumerable(Of clsImportedMessage), String, String, Boolean, clsDiagnostics) | clsImportReport | Create a new store at path and drop items into folderPath in one call — the common "author a PST from scratch" case. |
CreateStoreWithFolders(String, IEnumerable(Of clsImportedMessage), String, IEnumerable(Of String), String, Boolean, clsDiagnostics) | clsImportReport | Create a new store and populate MANY folders in one call — the "author a whole mailbox from scratch" case. One atomic write for the entire tree. |
DeleteFolder(String, String, String, Boolean, clsDiagnostics, Boolean) | clsEditReport | Delete the folder at folderPath. When recursive is False, refuses a non-empty folder. |
DeleteItems(String, String, String, Func(Of clsMessage, Boolean), clsDiagnostics, Boolean) | clsEditReport | Delete items in folderPath matching match (Nothing = all items in the folder), writing to outPath. |
EditItems(String, String, String, Func(Of clsMessage, Boolean), clsItemEdit, clsDiagnostics, Boolean) | clsEditReport | Edit the properties (subject / body / importance — see clsItemEdit) of items in folderPath matching match (Nothing = all), writing to outPath. The folder-list subject cache is kept in step. |
MoveFolder(String, String, String, String, clsDiagnostics, Boolean) | clsEditReport | Move the folder at folderPath under a new parent folder newParentPath, writing to outPath. |
MoveItems(String, String, String, Func(Of clsMessage, Boolean), String, clsDiagnostics, Boolean) | clsEditReport | Move items matching match (Nothing = all) from fromFolder to toFolder, writing to outPath. Counts on both folders are corrected and the moved items' NBT parent is re-pointed at the destination. |
RemoveAttachments(String, String, String, Func(Of clsMessage, Boolean), Func(Of clsAttachment, Boolean), clsDiagnostics, Boolean) | clsEditReport | Remove attachments matching attMatch (Nothing = all) from the messages in folderPath matching msgMatch (Nothing = all), writing to outPath. When a message loses its last attachment its PidTagHasAttachments and mfHasAttach flag are cleared. |
RenameFolder(String, String, String, String, clsDiagnostics, Boolean) | clsEditReport | Rename the folder at folderPath to newName, writing to outPath. |
SetFolderClass(String, String, String, String, clsDiagnostics, Boolean) | clsEditReport | Set the folder's container class (PidTagContainerClass) — what marks a folder as holding appointments, contacts or tasks rather than mail — writing to outPath. |
Bastion.Pst.Authoring
Accumulates rename / move / delete / remove-attachment edits and applies them in a single atomic, reopen-validated rewrite. Fluent: chain the queue methods, then Commit.
| Member | Type | Summary |
|---|---|---|
Count | Int32 | Number of edits queued so far. |
| Member | Type | Summary |
|---|---|---|
Commit(String, Boolean) | clsEditReport | Apply every queued edit to outPath in one atomic rewrite. The returned clsEditReport carries the combined per-kind change counts. |
DeleteFolder(String, Boolean) | clsAuthoringSession | |
DeleteItems(String, Func(Of clsMessage, Boolean)) | clsAuthoringSession | |
EditItems(String, Func(Of clsMessage, Boolean), clsItemEdit) | clsAuthoringSession | |
MoveFolder(String, String) | clsAuthoringSession | |
MoveItems(String, Func(Of clsMessage, Boolean), String) | clsAuthoringSession | |
RemoveAttachments(String, Func(Of clsMessage, Boolean), Func(Of clsAttachment, Boolean)) | clsAuthoringSession | |
RenameFolder(String, String) | clsAuthoringSession | |
SetFolderClass(String, String) | clsAuthoringSession | Set the folder's container class (PidTagContainerClass) — what marks it as holding appointments, contacts or tasks rather than mail. |
Bastion.Pst.Authoring
| Member | Type | Summary |
|---|---|---|
Address(String, String) | clsContactBuilder | Business and home postal addresses (each a single multi-line string). |
Anniversary(DateTime) | clsContactBuilder | |
Birthday(DateTime) | clsContactBuilder | |
Company(String) | clsContactBuilder | |
Department(String) | clsContactBuilder | |
DisplayName(String) | clsContactBuilder | |
Email(String, Int32) | clsContactBuilder | |
Fax(String) | clsContactBuilder | |
JobTitle(String) | clsContactBuilder | |
Name(String, String, String) | clsContactBuilder | |
Notes(String) | clsContactBuilder | Free-text notes — stored as the contact item's body. |
Phone(String, String, String) | clsContactBuilder | |
Photo(Byte[], String) | clsContactBuilder | Attach a contact photo (JPEG bytes). Written with PidTagAttachmentContactPhoto + PidLidHasPicture so Outlook shows it on the card. |
WebPage(String) | clsContactBuilder |
Bastion.Pst.Authoring
| Member | Type | Summary |
|---|---|---|
AddMember(String, String) | clsDistListBuilder | Add a one-off member (display name + SMTP address). |
Name(String) | clsDistListBuilder | The list's display name. |
Bastion.Pst.Authoring
Entry point for building typed items: clsItemBuilder.Mail(), .Appointment(), .Contact(), .Task(), .Note().
| Member | Type | Summary |
|---|---|---|
Appointment() | clsAppointmentBuilder | |
Contact() | clsContactBuilder | |
DistributionList() | clsDistListBuilder | |
Journal() | clsJournalBuilder | |
Mail() | clsMailBuilder | |
Note() | clsNoteBuilder | |
Rss() | clsRssBuilder | |
Task() | clsTaskBuilder |
Bastion.Pst.Authoring
Shared surface: subject/body/attachments and the terminal Build.
| Member | Type | Summary |
|---|---|---|
Build() | clsImportedMessage | The finished neutral item, ready for clsAuthor / clsStoreImporter. |
Bastion.Pst.Authoring
| Member | Type | Summary |
|---|---|---|
BodyText(String) | clsJournalBuilder | |
DurationMinutes(Int32) | clsJournalBuilder | |
EndsAt(DateTime) | clsJournalBuilder | |
JournalType(String) | clsJournalBuilder | Free-text activity type, e.g. "Phone call". |
StartsAt(DateTime) | clsJournalBuilder | |
Subject(String) | clsJournalBuilder |
Bastion.Pst.Authoring
| Member | Type | Summary |
|---|---|---|
AddAttachment(String, Byte[], String) | clsMailBuilder | |
AddEmbeddedMessage(String, clsImportedMessage) | clsMailBuilder | |
Bcc(String, String) | clsMailBuilder | |
BodyHtml(String) | clsMailBuilder | |
BodyText(String) | clsMailBuilder | |
Cc(String, String) | clsMailBuilder | |
From(String, String) | clsMailBuilder | |
Importance(Int32) | clsMailBuilder | 0 low, 1 normal, 2 high. |
MessageId(String) | clsMailBuilder | |
Read(Boolean) | clsMailBuilder | Mark the item read (default) or unread. |
SentOn(DateTime) | clsMailBuilder | |
Subject(String) | clsMailBuilder | |
To(String, String) | clsMailBuilder |
Bastion.Pst.Authoring
| Member | Type | Summary |
|---|---|---|
Color(Int32) | clsNoteBuilder | 0 blue, 1 green, 2 pink, 3 yellow, 4 white. |
Size(Int32, Int32) | clsNoteBuilder | |
Subject(String) | clsNoteBuilder | |
Text(String) | clsNoteBuilder |
Bastion.Pst.Authoring
An RSS feed item (IPM.Post.Rss).
| Member | Type | Summary |
|---|---|---|
Article(String, String) | clsRssBuilder | The article's own URL and feed-unique id. |
BodyHtml(String) | clsRssBuilder | |
BodyText(String) | clsRssBuilder | |
Channel(String, String) | clsRssBuilder | The feed this item came from: display name and channel URL. |
PostedOn(DateTime) | clsRssBuilder | |
Read(Boolean) | clsRssBuilder | |
Subject(String) | clsRssBuilder |
Bastion.Pst.Authoring
| Member | Type | Summary |
|---|---|---|
BodyText(String) | clsTaskBuilder | |
CompletedOn(DateTime) | clsTaskBuilder | |
DueDate(DateTime) | clsTaskBuilder | |
Owner(String) | clsTaskBuilder | |
PercentComplete(Double) | clsTaskBuilder | 0..1. |
StartDate(DateTime) | clsTaskBuilder | |
Status(Int32) | clsTaskBuilder | 0 not started, 1 in progress, 2 complete, 3 waiting, 4 deferred. |
Subject(String) | clsTaskBuilder |
Bastion.Pst.Convert
| Member | Type | Summary |
|---|---|---|
AcrossFolders | Boolean | Collapse duplicates across the WHOLE output instead of within each folder. Default False, which is the behaviour to keep unless a user has explicitly asked for otherwise. False (default): a message is a duplicate only of another copy in the SAME destination folder. The folder tree survives the merge exactly as the user built it — the same mail filed in "Inbox\Sales" and "Inbox\Sales 2025" is kept in both. True: one copy of each message survives anywhere in the output, so every other copy is removed no matter which folder it was filed in. This compacts the folder tree. Folders whose entire contents existed elsewhere are left empty, and with pruneEmpty set they are deleted outright — the user's filing is discarded, not just the redundant bytes. On a real 305 GB corpus this emptied 49 non-empty folders. Only set it where the person choosing has been told that plainly and wants the smallest possible output more than they want their folders. |
BaseDrops | Dictionary(Of UInt32, HashSet(Of UInt32)) | Base-store contents tables needing row drops (base-internal dedup): the first copy of each message is kept, later copies within the same store are dropped. Populated by Seed. |
BaseDuplicateCount | Int32 | Total base-internal duplicates marked for removal by Seed. |
Bastion.Pst.Convert
Thrown at the start of a write operation when the destination already exists and overwrite was not requested. The host application can catch this and prompt the user.
| Member | Type | Summary |
|---|---|---|
DestinationPath | String | The destination path that already exists. |
Bastion.Pst.Convert
Result of a store edit: the standard error contract plus how many items/folders changed.
| Member | Type | Summary |
|---|---|---|
AttachmentsRemoved | Int32 | |
FoldersDeleted | Int32 | |
FoldersMoved | Int32 | |
FoldersReclassified | Int32 | |
FoldersRenamed | Int32 | |
ItemsDeleted | Int32 | |
ItemsEdited | Int32 | |
ItemsMoved | Int32 |
Bastion.Pst.Convert
Property changes to apply to matching items (any field left Nothing is unchanged).
| Member | Type | Summary |
|---|---|---|
BodyText | String | New plain-text body, or Nothing to leave unchanged. |
Importance | Nullable(Of Int32) | New importance (0 low, 1 normal, 2 high), or Nothing to leave unchanged. |
Subject | String | New subject (also updates the folder-list cache), or Nothing to leave unchanged. |
Bastion.Pst.Convert
Scans merge sources for corruption before the merge begins, and repairs the ones the caller agrees to repair. See Scan.
| Member | Type | Summary |
|---|---|---|
Repair(clsPreflightReport, String, Func(Of clsSourceHealth, Int32, Int32, enmRepairChoice), clsDiagnostics) | clsPreflightReport | Repair the corrupt sources in scan, asking prompt what to do about each. "…ToAll" answers stop the asking, so an interactive host shows at most one dialog; pass a prompt that always returns enmRepairChoice.YesToAll for unattended use (or call ScanAndRepairAll). |
Scan(IList(Of String), clsDiagnostics) | clsPreflightReport | Validate every source and report which are damaged. Nothing is modified and nothing is repaired — this is the pass whose clsPreflightReport.CorruptCount feeds the single up-front prompt. |
ScanAndRepairAll(IList(Of String), String, clsDiagnostics) | clsPreflightReport | Scan, then repair every corrupt source without asking — the non-interactive default (equivalent to answering "Yes to all"). Repaired copies are written into workDir; the originals are never modified. |
Bastion.Pst.Convert
Outcome of a merge. ErrorCode 0 = success; on failure ErrorDescription says why (ToString() renders "code: description").
| Member | Type | Summary |
|---|---|---|
BlankMessagesRemoved | Int32 | Blank mail removed (no subject, no body, no attachments) — dropBlankMessages. |
DuplicatesRemoved | Int32 | |
FilteredOut | Int32 | Messages dropped by the item-type filter (selective merge). |
FoldersMerged | Int32 | |
FoldersPruned | Int32 | Empty folders removed by the prune pass (pruneEmpty). |
FoldersRehomed | Int32 | Real folders found outside the IPM subtree (parked under the store root, where Outlook never displays them) and re-homed under the IPM root so they are visible. |
MessagesMerged | Int32 | |
MessagesRehomed | Int32 | Messages owned by the store root folder itself (invisible to Outlook) and re-homed into the IPM root's contents table. |
TotalNodes | Int32 | |
Warning | String | Non-fatal pre-flight advisory (e.g. low disk space) — Nothing when clean. |
Bastion.Pst.Convert
Outcome of clsStorePassword.SetPassword / clsStorePassword.RemovePassword: what was written, the structural validation of the output, and the password state read back from the output as evidence.
| Member | Type | Summary |
|---|---|---|
HadPassword | Boolean | Whether the source had a password before the change. |
HasPassword | Boolean | Whether the output has a password now (read back from the output file). |
NodesWritten | Int32 | |
OutputIsValid | Boolean | True when the output passes strict structural validation. |
OutputPath | String | |
OutputReport | clsValidationReport | Strict structural validation of the output. |
PasswordCrc | UInt32 | The CRC value written (0 = removed). |
SourcePath | String | |
Succeeded | Boolean | Whether the change completed (ErrorCode 0) AND produced a structurally-valid file. |
Bastion.Pst.Convert
Result of clsStorePassword.GetStatus: whether the file has an open password and the stored CRC value.
| Member | Type | Summary |
|---|---|---|
HasPassword | Boolean | True when PidTagPstPassword is present and non-zero. |
PasswordCrc | UInt32 | The stored CRC (0 = no password). Compare with clsStorePassword.ComputeCrc to verify a candidate password. |
Path | String |
Bastion.Pst.Convert
Outcome of a pre-merge scan (and repair, when one was run).
| Member | Type | Summary |
|---|---|---|
Cancelled | Boolean | True when the caller answered enmRepairChoice.Cancel. The merge must not proceed. |
CorruptCount | Int32 | How many sources are damaged and repairable. This is the number to put in the single prompt: "Found {CorruptCount} corrupted PST files. Do you want to attempt repair(s)?" |
EffectiveSources | IList(Of String) | The list to hand to the merger: repaired copies substituted for the originals they replace, unsupported formats still present (the merge's own recovery path reports them). |
Files | IList(Of clsSourceHealth) | Every source, in the order supplied. |
LegacyAnsiCount | Int32 | How many sources are legacy ANSI stores. They are sound and merge normally; they are counted separately only so they are never mistaken for damage. |
RepairedCount | Int32 | How many sources were successfully repaired. |
RepairFailedCount | Int32 | How many repairs were attempted and failed. |
UnsupportedCount | Int32 | How many sources are a format we do not read (Outlook 2013+ OSTs). These are not damaged and are not offered for repair — they simply cannot take part in the merge. |
| Member | Type | Summary |
|---|---|---|
Describe() | String | One line per problem file, fit to show a user. |
Bastion.Pst.Convert
One source file's pre-merge verdict.
| Member | Type | Summary |
|---|---|---|
EffectivePath | String | The path a merge should actually consume: the repaired copy when there is one, otherwise the original. |
ErrorCount | Int32 | Structural errors the validator found. |
IsCorrupted | Boolean | True when the file has structural errors, or could not be read at all. |
IsLegacyAnsi | Boolean | True when the file is a legacy ANSI store (wVer 14/15). It is NOT damaged. The structural validator only understands the Unicode container, so it reports every ANSI store as having a header error; treating that as corruption would "repair" a perfectly good file by rewriting it as Unicode — an upgrade Outlook 2002 and earlier cannot open, and one the library deliberately gates behind AnsiUpgradeRequired everywhere else. |
IsUnsupportedFormat | Boolean | True when the file is a format this library does not read — an Outlook 2013+ OST (wVer 36). Such a file is NOT damaged and repairing it cannot work, so it is reported separately and never offered for repair. |
Path | String | The source file as supplied. |
RepairAttempted | Boolean | True once a repair was attempted for this file. |
RepairedPath | String | The repaired copy, when one was written. The original is never modified. |
RepairSucceeded | Boolean | True when the repair produced a usable file. |
Summary | String | One line fit to show a user. |
SupportReportPath | String | Where the support report was written, when one was. |
Bastion.Pst.Convert
| Member | Type | Summary |
|---|---|---|
MessageCount | Int32 | |
Path | String | |
SizeBytes | Int64 |
Bastion.Pst.Convert
Outcome of a split. ErrorCode 0 = success.
| Member | Type | Summary |
|---|---|---|
Parts | List(Of clsSplitPart) | |
TotalMessages | Int32 |
Bastion.Pst.Convert
| Member | Type | Summary |
|---|---|---|
ConvertToUnicode(String, String, clsDiagnostics, Boolean) | clsGenericResult | Convert a PST/OST (ANSI or Unicode) into a fresh Unicode PST at outPath. ANSI stores are upgraded to Unicode (escaping the 2 GB limit); Table Contexts are rebuilt at Unicode geometry so other clients read them. NEVER throws: check the result's ErrorCode (0 = success). |
ConvertToUnicode(String, String) | clsGenericResult | Convert a PST/OST (ANSI or Unicode) into a fresh Unicode PST at outPath. ANSI stores are upgraded to Unicode (escaping the 2 GB limit); Table Contexts are rebuilt at Unicode geometry so other clients read them. NEVER throws: check the result's ErrorCode (0 = success). |
Bastion.Pst.Convert
| Member | Type | Summary |
|---|---|---|
DropSearchFolders | Boolean | When True (default) merges drop search/view folders (non-IPM root children). Set False (CLI --keep-search) to carry them verbatim. |
MaxNodeIdsPerVolume | UInt64 | Max cumulative source node-id span packed into one output volume (MergeToVolumes) before rolling to a new volume. 0 = default (85% of the 27-bit nidIndex ceiling). Lower it in tests to force multi-volume source splitting without needing hundreds of real sources. |
MergeWorkers | Int32 | Max concurrent source-conversion workers for the parallel merge. |
ParallelMerge | Boolean | When True (default) a plain merge (no dedup/type-filter/volume edits) converts its sources on parallel worker threads, drained in source order. Set False to force the single-threaded pipeline (diagnostic / byte-equivalence A-B). |
| Member | Type | Summary |
|---|---|---|
Merge(IList(Of String), String, clsDedup, HashSet(Of enmItemKind), clsDiagnostics, Boolean, Func(Of Int32, Dictionary(Of UInt32, HashSet(Of UInt32))), Int64, enmMergeLayout, String, Boolean, Boolean) | clsMergeReport | Merge sourcePaths into a new Unicode PST at outPath. dedup removes duplicate messages. rootName sets the store display name Outlook shows for the mounted PST (default: "Stacked Merge" / "Unified Merge" by layout). NEVER throws: the returned report's ErrorCode is 0 on success, otherwise a failure class with ErrorDescription (report.ToString() = "code: description"). |
MergeToVolumes(IList(Of String), String, Int64, clsDedup, HashSet(Of enmItemKind), clsDiagnostics, Boolean, String, Boolean, Boolean, enmMergeLayout) | clsVolumeReport | Merge sourcePaths into one or more Unicode PST volumes capped at capBytes of message data each: outBasePath (archive.pst), then archive-002.pst, archive-003.pst … Each volume is a standalone PST carrying the full folder structure with its share of the messages. Duplicates are removed globally (once, in a pre-scan) so no message appears in more than one volume. Memory stays bounded (the scan holds per-message metadata; each volume streams). |
OrphanScan(String) | ValueTuple(Of Int32, Int32, List(Of UInt32)) | Message nodes present in the NBT but referenced by NO contents/assoc table row — invisible to Outlook and to every row-based gate (count, conservation, inventory), yet still occupying the store. Rows and nodes are dropped by SEPARATE mechanisms, so a bug can remove one and not the other: a rewrite that drops a message's table ROW must drop its NODE too. Read-only; computed from a single NBT pass. |
SweepPlan(String) | ValueTuple(Of Int32, Int32, Int32, Int32) | What a post-merge sweep would actually change, computed READ-ONLY. A sweep is a whole-store rewrite (10–22GB for a real archive), so discovering "nothing to do" by writing the file and then deleting it is the expensive way to learn it. Plan first; rewrite only when a count is non-zero. |
Bastion.Pst.Convert
Read, set or remove the password of a PST file. A PST password is a stored CRC hash, not encryption — it controls Outlook's open prompt only; the data itself is never encrypted by it. SetPassword/RemovePassword write a new file (atomic, validated): pass the same path as source and output with overwrite:=True to change in place.
| Member | Type | Summary |
|---|---|---|
ComputeCrc(String) | UInt32 | The CRC Outlook stores and compares for password — the [MS-PST] 5.3 CRC of the password's ANSI (Windows-1252) bytes. Empty/Nothing returns 0 (= no password). Exposed so hosts can verify a user-supplied password against clsPasswordStatus.PasswordCrc before opening. |
GetStatus(String) | clsPasswordStatus | Report whether path has an open password set (PidTagPstPassword present and non-zero), without throwing. |
RemovePassword(String, String, clsDiagnostics, Boolean) | clsPasswordChangeReport | Write a copy of srcPath to outPath with the open password cleared, so the file opens without a prompt. Same guarantees and in-place convention as SetPassword. |
SetPassword(String, String, String, clsDiagnostics, Boolean) | clsPasswordChangeReport | Write a copy of srcPath to outPath with the open password set to password. Only the message store PC is rebuilt — every other node is preserved byte-for-byte (an ANSI source is upgraded to Unicode, as everywhere else in the library). The output is written atomically and structurally validated; the new CRC is read back from the output as proof. To change the password of a file in place, pass the same path for both arguments with overwrite:=True. |
Bastion.Pst.Convert
| Member | Type | Summary |
|---|---|---|
SplitByPredicate(String, String, String, Func(Of clsMessage, Boolean), clsDiagnostics) | clsSplitReport | Split into two stores: messages that satisfy predicate go to matchPath, the rest to restPath. The predicate receives each clsMessage. NEVER throws: check the report's ErrorCode (0 = success). |
SplitBySize(String, String, Int64, clsDiagnostics) | clsSplitReport | Split sourcePath into parts of at most maxBytes of message data each, written as <name>-001.pst, -002.pst, … in outDir. NEVER throws: check the report's ErrorCode (0 = success). |
Bastion.Pst.Convert
One output volume produced by a capped merge.
| Member | Type | Summary |
|---|---|---|
Messages | Int32 | |
Path | String | |
SizeBytes | Int64 |
Bastion.Pst.Convert
Outcome of a capped (volume-split) merge. ErrorCode 0 = success.
| Member | Type | Summary |
|---|---|---|
BlankMessagesRemoved | Int32 | Blank mail removed (no subject, no body, no attachments). |
DuplicatesRemoved | Int32 | |
TotalMessages | Int32 | |
Volumes | List(Of clsVolumeInfo) | |
Warning | String | Non-fatal pre-flight advisory (e.g. low disk space) — Nothing when clean. |
Bastion.Pst.Convert
How a merge lays out the sources in the output store.
| Member | Value |
|---|---|
Stacked | 0 |
Unified | 1 |
Bastion.Pst.Convert
What the caller wants done about one corrupt source. The "…ToAll" answers apply to every remaining corrupt file so the user is not asked again; Cancel abandons the whole operation.
| Member | Value |
|---|---|
Yes | 0 |
YesToAll | 1 |
No | 2 |
NoToAll | 3 |
Cancel | 4 |
Bastion.Pst.Diagnostics
Payload for clsDiagnostics.AnsiUpgradeRequired: the ANSI source about to be upgraded to Unicode. Set Proceed to make the hard choice — True upgrades to Unicode (unopenable in Outlook 2002 and earlier), False aborts with the source PST untouched.
| Member | Type | Summary |
|---|---|---|
Proceed | Boolean | The decision: True to upgrade to Unicode, False (default) to abort without writing. |
SourcePath | String | The legacy ANSI PST that the operation would rewrite as Unicode. |
Bastion.Pst.Diagnostics
Payload for clsDiagnostics.SourceCorruptionDetected: which file, how bad, and (when auto-generated) where the developer support report was written.
| Member | Type | Summary |
|---|---|---|
ErrorCount | Int32 | Structural error count the scan found. |
Path | String | |
Summary | String | Sample of issue descriptions (offset + reason), enough for a log line. |
SupportReportPath | String | Path of the auto-generated support report, or Nothing. |
Bastion.Pst.Diagnostics
Payload for clsDiagnostics.Started / clsDiagnostics.Completed.
| Member | Type | Summary |
|---|---|---|
Message | String | |
Operation | String | |
TimestampUtc | DateTime |
Bastion.Pst.Diagnostics
Opt-in diagnostics surface for read/export operations: switches plus public events the host application can subscribe to. Pass an instance into Merge/Split/ConvertToUnicode.
| Member | Type | Summary |
|---|---|---|
AllowAnsiToUnicodeUpgrade | Boolean | Explicit, up-front authorisation to rewrite a legacy ANSI PST as Unicode when adding to / editing it. Default False: such an operation is REFUSED (returns enmErrorCode.AnsiUpgradeRequired, source untouched) unless this is set True or an AnsiUpgradeRequired handler proceeds. Guards ANSI-only-Outlook users against a silent format upgrade that would make the file unopenable in their Outlook. |
AutoSupportReport | Boolean | When true and corruption is detected, a support report (structural metadata only — never message content) is written automatically; its path rides on the SourceCorruptionDetected event and in the user log. |
CancellationToken | CancellationToken | Cooperative cancellation for long operations (merge/split/convert/import). Pass a token from your CancellationTokenSource; when you cancel it, the operation stops at the next checkpoint and returns enmErrorCode.OperationCancelled. The atomic-write model means nothing partial is left behind — the destination file is untouched. |
CorruptionDetected | Boolean | True once any scanned source (or a repair's source validation) reported corruption. |
CreateUserLog | Boolean | When true, the library writes a plain-language activity log a NON-developer can read — what was read, what was merged, how many duplicates were removed, whether corruption was found and what was done about it. One line per milestone, no jargon, no offsets. Path in UserLogPath (default: BastionPstSdk-UserLog-….txt in the user's temp folder). |
EnableLogging | Boolean | When true, every raised event is also written, in detail, to LogFilePath. |
InMemoryThresholdBytes | Int64 | In Auto mode, inputs at or below this size are buffered in memory; larger inputs stream. Default 256 MB. (Streaming is safe at any size, so this is only a small-input optimisation.) |
LogFilePath | String | Path of the diagnostics log. If left blank when logging is first used, it defaults to %TEMP%\BastionPstSdk-yyyyMMdd-HHmmss-fff.log. |
ProcessingMode | enmProcessingMode | Processing mode. Auto (default) decides per input from its size; force InMemory or Streaming to override. Streaming has bounded memory for any size; the two produce identical output. |
RecoverFromCorruption | Boolean | When true, a read or write error on a single item is logged and reported via ErrorOccurred (Recovered=True) and the operation continues, exporting whatever it can recover, instead of aborting. |
ScanSourcesOnOpen | Boolean | When true, every source store is structurally scanned as an operation opens it (full page/block validation — thorough but adds read time). A corrupt source raises SourceCorruptionDetected, latches CorruptionDetected, and — when AutoSupportReport is set — writes a support report for the developer. |
SkipDuplicates | Boolean | When true, a message that already exists in the output is not written again (duplicate detection by PidTagInternetMessageId, else a subject/sender/time hash). |
SupportContact | String | Where the end user should send support reports — embedded in the report and the user-log guidance line. Defaults to Bastion Software Solutions Ltd's support address; set your own to route reports to your application's support instead. |
SupportReportDirectory | String | Directory for auto-generated support reports (default: the user's temp folder). |
UserLogLines | IReadOnlyList(Of String) | Everything written to the user log so far — hosts can show it in a UI instead of (or as well as) the file. |
UserLogPath | String | Path of the user log. If blank when the first line is written, defaults to %TEMP%\BastionPstSdk-UserLog-yyyyMMdd-HHmmss.txt. |
Bastion.Pst.Diagnostics
Payload for clsDiagnostics.ErrorOccurred: the failing phase, the error type and message, the item that failed, and whether the run recovered (skipped and continued).
| Member | Type | Summary |
|---|---|---|
Context | String | What was being processed (e.g. a node NID or source path). |
Error | Exception | The underlying exception (may be Nothing). |
ErrorType | String | The exception type name, e.g. "EndOfStreamException". |
Message | String | |
Phase | enmDiagPhase | |
Recovered | Boolean | True when the operation skipped the failing item and carried on (RecoverFromCorruption); False when the error aborts the operation. |
TimestampUtc | DateTime |
Bastion.Pst.Diagnostics
Payload for clsDiagnostics.ModeChosen: which mode an input was processed in.
| Member | Type | Summary |
|---|---|---|
Mode | enmProcessingMode | |
Operation | String | |
Path | String | |
SizeBytes | Int64 |
Bastion.Pst.Diagnostics
Payload for clsDiagnostics.ReadProgress / clsDiagnostics.WriteProgress.
| Member | Type | Summary |
|---|---|---|
CurrentItem | String | |
PercentComplete | Double | 0..100, or -1 when Total is unknown. |
Phase | enmDiagPhase | |
Processed | Int64 | |
TimestampUtc | DateTime | |
Total | Int64 | Total items, or -1 when not known in advance. |
Bastion.Pst.Diagnostics
Which side of an operation an event refers to.
| Member | Value |
|---|---|
Read | 0 |
Write | 1 |
Bastion.Pst.Diagnostics
How a single input store is processed.
| Member | Value |
|---|---|
Auto | 0 |
InMemory | 1 |
Streaming | 2 |
Bastion.Pst.Export
Result of a bulk export (a whole PST to a folder of files): the standard error contract plus how many items were written and how many were skipped.
| Member | Type | Summary |
|---|---|---|
Failed | Int32 | Number of items that could not be exported (each is noted in the diagnostics/user log); the overall operation still succeeds unless it could not start at all. |
FilesWritten | Int32 | Number of items successfully written to disk. |
FoldersVisited | Int32 | Number of folders visited. |
ItemsFound | Int32 | Number of items encountered while walking the store, counted before any format filter is applied. This is what tells "the store holds nothing" apart from "the store holds nothing of the kind this format can carry" when FilesWritten is zero — an empty output folder looks identical in both cases. |
Warning | String | Non-fatal note (e.g. "N item(s) skipped"), or Nothing. |
Bastion.Pst.Export
Exports non-mail items to their standard interchange formats: appointments to iCalendar (.ics, RFC 5545) and contacts to vCard (.vcf, RFC 6350). Clean-room from the public RFCs; never throws.
| Member | Type | Summary |
|---|---|---|
ExportIcs(clsMessage, String, Boolean) | clsFileOpResult | Write an appointment message to an iCalendar (.ics) file. |
ExportVcf(clsContactView, String, Boolean) | clsFileOpResult | Write a contact message to a vCard (.vcf) file. |
ExportVcf(clsMessage, String, Boolean) | clsFileOpResult | Write a contact message to a vCard (.vcf) file. |
ToIcs(clsMessage) | String | Return the appointment as an iCalendar (VCALENDAR/VEVENT) string, or "" if the message is not an appointment. |
ToVcf(clsContactView) | String | Return the contact as a vCard 3.0 string, or "" if the message is not a contact. |
ToVcf(clsMessage) | String | Return the contact as a vCard 3.0 string, or "" if the message is not a contact. |
Bastion.Pst.Export
Exports a single message to an interchange format. First format: EML (RFC 5322 / MIME). Clean-room from the public RFCs; never throws — returns a clsFileOpResult.
| Member | Type | Summary |
|---|---|---|
ExportEml(clsMessage, String, Boolean) | clsFileOpResult | Write msg to path as a MIME .eml file. |
ExportMhtml(clsMessage, String, Boolean) | clsFileOpResult | Write msg to path as MIME HTML (.mhtml, RFC 2557) — a single-file web archive (HTML body plus inline resources). |
ExportMsg(clsMessage, String, Boolean) | clsFileOpResult | Write msg to path as an Outlook .msg file ([MS-OXMSG] compound file). |
ExportOft(clsMessage, String, Boolean) | clsFileOpResult | Write msg to path as an Outlook template (.oft): an [MS-OXMSG] compound file with the template root CLSID and the message marked unsent — the form Outlook loads from "Choose Form / User Templates". |
ToEml(clsMessage) | String | Return msg as a MIME (.eml) string. |
ToMhtml(clsMessage) | String | Return msg as an MHTML (.mhtml) string. |
Bastion.Pst.Export
Creates new, empty Outlook data files. CreateEmpty writes a fresh, structurally-valid PST that mounts in Outlook — ready for a developer to merge other stores into. Never throws.
| Member | Type | Summary |
|---|---|---|
CreateEmpty(String, Boolean, String) | clsFileOpResult | Create a new, empty PST at path — an Outlook-mountable store with the standard root hierarchy and no messages. The output is validated (reopened) before the call returns. |
Bastion.Pst.Export
Bulk export: walk a PST/OST and write every message to its own interchange file, mirroring the folder tree on disk. Never throws — returns a clsExportReport.
| Member | Type | Summary |
|---|---|---|
ExportToFolder(String, String, enmExportFormat, clsDiagnostics, Boolean) | clsExportReport | Export every message in pstPath to outDir, one file per message under a folder tree that mirrors the store. A failure to export a single item is skipped (counted in clsExportReport.Failed and noted in the user log), not fatal. |
Bastion.Pst.Export
Interchange formats Bastion PST SDK can export an item to. More are added per the export roadmap (MSG, MHTML, ICS, VCF, MBOX, OFT); EML is the first.
| Member | Value |
|---|---|
Eml | 0 |
Msg | 1 |
Ics | 2 |
Vcf | 3 |
Mhtml | 4 |
Mbox | 5 |
Oft | 6 |
Bastion.Pst.Import
Parses an RFC 5322 / MIME message (.eml) into a clsImportedMessage. Clean-room from the public RFCs; validates the input and never throws.
| Member | Type | Summary |
|---|---|---|
Parse(Byte[]) | clsImportResult | Parse raw .eml bytes into a message model. |
ParseFile(String) | clsImportResult | Parse an .eml file into a message model. |
Bastion.Pst.Import
Reads an iCalendar (.ics) file: yields each VEVENT as an appointment-kind clsImportedMessage. Malformed events are skipped; never throws on content.
| Member | Type | Summary |
|---|---|---|
Parse(String) | List(Of clsImportedMessage) | Parse iCalendar text: one imported appointment per VEVENT. |
ReadFile(String) | IEnumerable(Of clsImportedMessage) | Enumerate every VEVENT in an .ics file as an imported appointment. |
Bastion.Pst.Import
A name/email pair parsed from an address header.
| Member | Type | Summary |
|---|---|---|
Email | String | |
Name | String |
| Member | Type | Summary |
|---|---|---|
ToString() | String |
Bastion.Pst.Import
Appointment fields parsed from an iCalendar VEVENT (RFC 5545). Attendees ride in the owning message's To list; SUMMARY/DESCRIPTION in Subject/BodyText.
| Member | Type | Summary |
|---|---|---|
BusyStatus | Int32 | olBusyStatus: 0 free, 1 tentative, 2 busy, 3 out of office. |
EndTime | Nullable(Of DateTime) | |
IsAllDay | Boolean | True for a VALUE=DATE (all-day) event. |
Location | String | |
Recurrence | clsImportedRecurrence | Recurrence pattern, or Nothing for a single-instance appointment. |
ReminderMinutesBeforeStart | Int32 | |
ReminderSet | Boolean | |
StartTime | Nullable(Of DateTime) | |
Uid | String | The event's UID, also mirrored into the message's MessageId. |
Bastion.Pst.Import
An attachment parsed from an imported message.
| Member | Type | Summary |
|---|---|---|
ContentId | String | |
Data | Byte[] | |
EmbeddedMessage | clsImportedMessage | Set when the attachment is itself a message (message/rfc822 part in EML, nested .msg storage in MSG): the parsed message. The importer writes it into the PST as a true embedded-message attachment (afEmbeddedMessage). Data keeps the raw bytes (when the source had them) purely as a fallback. |
FileName | String | |
IsContactPhoto | Boolean | True for a contact's photo: written with PidTagAttachmentContactPhoto so Outlook renders it as the contact picture rather than a normal file attachment. |
IsInline | Boolean | |
MimeType | String |
Bastion.Pst.Import
Contact fields parsed from a vCard (RFC 6350 / 2425; 2.1–4.0 tolerated).
| Member | Type | Summary |
|---|---|---|
Anniversary | Nullable(Of DateTime) | |
Birthday | Nullable(Of DateTime) | |
BusinessAddress | String | Single-string postal addresses (the reader joins ADR components). |
BusinessFax | String | |
BusinessTelephone | String | |
CompanyName | String | |
Department | String | |
DisplayName | String | |
Email1Address | String | |
Email2Address | String | |
Email3Address | String | |
GivenName | String | |
HomeAddress | String | |
HomeTelephone | String | |
JobTitle | String | |
MiddleName | String | |
MobileTelephone | String | |
Surname | String | |
WebPage | String |
Bastion.Pst.Import
Distribution-list fields ([MS-OXOABK]): the list name and its one-off members.
| Member | Type | Summary |
|---|---|---|
Members | List(Of clsImportedDistListMember) | |
Name | String |
Bastion.Pst.Import
A distribution-list member (a one-off recipient: display name + SMTP address).
| Member | Type | Summary |
|---|---|---|
DisplayName | String | |
Email | String |
Bastion.Pst.Import
Journal-entry fields ([MS-OXOJRNL]).
| Member | Type | Summary |
|---|---|---|
DurationMinutes | Int32 | |
EndTime | Nullable(Of DateTime) | |
JournalType | String | Free-text activity type, e.g. "Phone call", "E-mail Message". |
StartTime | Nullable(Of DateTime) |
Bastion.Pst.Import
A message read from an interchange file (EML, MSG, MBOX). This is the neutral, fully-materialised model the import readers produce — inspect it, or hand it to an exporter to convert between formats.
| Member | Type | Summary |
|---|---|---|
Appointment | clsImportedAppointment | Appointment payload when Kind is Appointment, else Nothing. |
Attachments | List(Of clsImportedAttachment) | |
Bcc | List(Of clsImportedAddress) | |
BodyHtml | String | |
BodyText | String | |
Cc | List(Of clsImportedAddress) | |
Contact | clsImportedContact | Contact payload when Kind is Contact, else Nothing. |
Date | Nullable(Of DateTime) | |
DistList | clsImportedDistList | Distribution-list payload when Kind is DistributionList, else Nothing. |
FromEmail | String | |
FromName | String | |
Headers | List(Of KeyValuePair(Of String, String)) | All raw header name/value pairs, in order. |
Importance | Int32 | PidTagImportance: 0 low, 1 normal (default), 2 high. |
IsRead | Boolean | Whether the item is marked read (MSGFLAG_READ). Defaults True, which is what an imported item has always been; set False to author an unread item. |
Journal | clsImportedJournal | Journal payload when Kind is Journal, else Nothing. |
Kind | enmImportedItemKind | Item kind: Mail (default), or Appointment / Contact from the ICS / VCF readers. The importer composes the matching Outlook item class. |
MessageId | String | |
Note | clsImportedNote | Note payload when Kind is Note, else Nothing. |
Rss | clsImportedRss | RSS-post payload when Kind is Rss, else Nothing. |
SourceFolder | String | The folder path this item came from (set by readers that carry a folder tree, e.g. the OLM reader). Empty for flat interchange files. Used to place the item on import. |
Subject | String | |
Task | clsImportedTask | Task payload when Kind is Task, else Nothing. |
To | List(Of clsImportedAddress) |
| Member | Type | Summary |
|---|---|---|
ToString() | String |
Bastion.Pst.Import
Sticky-note fields ([MS-OXONOTE]). The note text is the message BodyText. Color: 0 blue,1 green,2 pink,3 yellow (default),4 white.
| Member | Type | Summary |
|---|---|---|
Color | Int32 | |
Height | Int32 | |
Width | Int32 |
Bastion.Pst.Import
An override of ONE occurrence of a recurring series ([MS-OXOCAL] ExceptionInfo / ExtendedException): the occurrence generated on OriginalDate is moved and/or re-titled. Members left unset inherit the series value.
| Member | Type | Summary |
|---|---|---|
NewBusyStatus | Nullable(Of Int32) | New olBusyStatus (0 free, 1 tentative, 2 busy, 3 OOF). Nothing = unchanged. |
NewEnd | Nullable(Of DateTime) | New end. Nothing = NewStart + the series duration. |
NewLocation | String | New location for this occurrence only. Nothing = unchanged. |
NewStart | Nullable(Of DateTime) | New start (date + time). Nothing = same day at the series start time. |
NewSubject | String | New subject for this occurrence only. Nothing = unchanged. |
OriginalDate | DateTime | The day of the generated occurrence being overridden (time is ignored). |
Bastion.Pst.Import
Recurrence pattern for an authored appointment ([MS-OXOCAL]). Supports daily (every N days), weekly (on selected weekdays) and monthly (on a day-of-month) series, ending after a count, on a date, or never.
| Member | Type | Summary |
|---|---|---|
DayOfMonth | Int32 | For Monthly: day of the month (1–31; 31 = last day). |
DaysOfWeek | enmRecurDays | For Weekly: which weekdays the occurrence falls on. |
EndDate | Nullable(Of DateTime) | End on/after this date (Nothing = count-based or never-ending). |
ExcludedDates | List(Of DateTime) | Occurrence dates to DELETE from the series (e.g. skip a holiday). Each must fall on a date the pattern would otherwise generate; the day is what matters (time is ignored). |
Frequency | enmRecurFrequency | |
Interval | Int32 | Interval: every N days / weeks / months. |
OccurrenceCount | Int32 | End after this many occurrences (0 = use EndDate or run forever). |
OverriddenOccurrences | List(Of clsImportedOccurrenceOverride) | Single occurrences to OVERRIDE (move / re-title) — [MS-OXOCAL] exceptions. Each must target a date the pattern generates and not one in ExcludedDates. |
Bastion.Pst.Import
RSS post fields ([MS-OXOPOST] / PSETID_PostRss). The article body is the message BodyText/BodyHtml; the channel is the feed the item arrived from.
| Member | Type | Summary |
|---|---|---|
Channel | String | The channel's display name (PidLidPostRssChannel). |
ChannelUrl | String | The feed's channel URL (PidLidPostRssChannelLink). |
ItemGuid | String | The article's feed-unique id (PidLidPostRssItemGuid). |
ItemUrl | String | The article's own URL (PidLidPostRssItemLink). |
Bastion.Pst.Import
Task fields ([MS-OXOTASK]). Status 0=not started,1=in progress,2=complete, 3=waiting,4=deferred; PercentComplete is 0..1.
| Member | Type | Summary |
|---|---|---|
DateCompleted | Nullable(Of DateTime) | |
DueDate | Nullable(Of DateTime) | |
Owner | String | |
PercentComplete | Double | |
StartDate | Nullable(Of DateTime) | |
Status | Int32 |
Bastion.Pst.Import
Result of an import run: the standard error contract plus counts. A per-file parse failure does not fail the run — it is recorded in Failures and the remaining files import normally; the run fails only when NO message could be imported.
| Member | Type | Summary |
|---|---|---|
Failures | List(Of String) | One line per failed input file: "path: reason". |
FilesFailed | Int32 | Input files that could not be parsed (see Failures). |
FilesRead | Int32 | Input files successfully parsed. |
FoldersCreated | Int32 | Folders created to satisfy the target folder path. |
MessagesImported | Int32 | Messages written into the output store. |
| Member | Type | Summary |
|---|---|---|
ToString() | String |
Bastion.Pst.Import
Result of importing a single message: the standard error contract plus the message.
| Member | Type | Summary |
|---|---|---|
Message | clsImportedMessage |
Bastion.Pst.Import
Reads a Unix mbox file: yields each message as a clsImportedMessage by splitting on "From " separator lines and parsing each block as MIME. Never throws.
| Member | Type | Summary |
|---|---|---|
ReadFile(String) | IEnumerable(Of clsImportedMessage) | Enumerate every message in an mbox file. Malformed blocks are skipped. |
Bastion.Pst.Import
Parses an Outlook .msg file ([MS-OXMSG] over [MS-CFB]) into a clsImportedMessage. Clean-room; validates the container and never throws.
| Member | Type | Summary |
|---|---|---|
Parse(Byte[]) | clsImportResult | Parse raw .msg bytes into a message model. |
ParseFile(String) | clsImportResult | Parse a .msg file into a message model. |
Bastion.Pst.Import
Reads Outlook-for-Mac .olm archives (mail items, with folder placement) into the neutral import model. Contacts/calendar/etc. live in separate consolidated XML files and are a later increment; this reads the per-message mail (message_NNNNN.xml).
| Member | Type | Summary |
|---|---|---|
ReadFile(String) | IEnumerable(Of clsImportedMessage) | Every mail message in the .olm, each with its clsImportedMessage.SourceFolder set from the archive path. Never throws — returns what parsed. |
Bastion.Pst.Import
Imports EML / MSG / MBOX / ICS / VCF files into a folder of a PST. pstPath is the store to import into (its content is preserved); pass Nothing/empty to start from a new, empty store. The result is always written to outPath — the input store is never modified. Never throws.
| Member | Type | Summary |
|---|---|---|
CloneStructure(String, String, clsDiagnostics, Boolean, String) | clsImportReport | Create a new, EMPTY Unicode PST at outPath that mirrors the folder hierarchy of sourcePath (no messages copied). Returns a clsImportReport whose FoldersCreated counts the folders made. |
CreateFolders(String, String, String, clsDiagnostics, Boolean, String) | clsImportReport | Create an (empty) folder path in pstPath (or a fresh store when empty), writing the result to outPath. Folders already present are left as they are. Returns a clsImportReport whose FoldersCreated counts new folders. |
ImportFile(String, String, String, String, clsDiagnostics, Boolean, String) | clsImportReport | Import one file by extension: .msg, .mbox, .ics (appointments), .vcf (contacts); anything else parses as EML/MIME. storeName (optional) sets the name Outlook shows for the store — use it when creating a new store (pstPath = Nothing) to give it a meaningful title. |
ImportFiles(String, String, IEnumerable(Of String), String, clsDiagnostics, Boolean, String) | clsImportReport | Import a set of EML / MSG / MBOX / ICS / VCF files into folderPath (created if missing; "/" separates subfolders, relative to the IPM root). storeName (optional) sets the store's display name shown in Outlook. |
ImportMbox(String, String, String, String, clsDiagnostics, Boolean, String) | clsImportReport | Import every message of an mbox file into folderPath. storeName (optional) sets the store's display name shown in Outlook. |
ImportMessages(String, String, IEnumerable(Of clsImportedMessage), String, clsDiagnostics, Boolean, String) | clsImportReport | Import already-parsed messages (e.g. built or edited in code) into folderPath of the store. storeName (optional) sets the store's display name shown in Outlook. |
ImportMessagesByFolder(String, String, IEnumerable(Of clsImportedMessage), String, IEnumerable(Of String), clsDiagnostics, Boolean, String) | clsImportReport | Import already-parsed messages into MANY folders in a SINGLE atomic rewrite. Each message is placed by its clsImportedMessage.SourceFolder, falling back to defaultFolder when that is empty; extraFolders names folders to create that receive no items. Equivalent to one ImportMessages call per folder, except the store is read and rewritten once rather than once per folder — the difference between O(S) and O(S·k) I/O when populating a store with many folders. |
ImportOlm(String, String, clsDiagnostics, Boolean, String) | clsImportReport | Import a Microsoft Outlook for Mac .olm archive into a new PST at outPath, preserving the OLM folder hierarchy. Returns a clsImportReport (ErrorCode 0 = success). |
Bastion.Pst.Import
Decodes TNEF (winmail.dat) streams into real attachments ([MS-OXTNEF]). Tolerant of damage; never throws into the caller.
| Member | Type | Summary |
|---|---|---|
IsTnef(Byte[]) | Boolean | True when the buffer starts with the TNEF signature. |
Parse(Byte[]) | clsTnefResult | Decode a TNEF stream. Damage is tolerated: unreadable tails are dropped, checksum mismatches are counted, and everything recoverable is returned. |
ParseFile(String) | clsTnefResult | Decode a TNEF file (e.g. a saved winmail.dat). |
| Member | Type | Summary |
|---|---|---|
Signature | UInt32 | The TNEF stream signature ([MS-OXTNEF] 2.1.3.1). |
Bastion.Pst.Import
Result of decoding a TNEF stream: the standard error contract, the recovered attachments and bodies, plus damage counters (bad checksums / truncated attributes).
| Member | Type | Summary |
|---|---|---|
Attachments | List(Of clsImportedAttachment) | The real attachments recovered from the TNEF stream. |
AttributesRead | Int32 | Attributes successfully walked. |
BodyHtml | String | HTML body carried in the MAPI property list (PidTagBodyHtml), if any. |
BodyText | String | Plain-text body carried in attBody, if any. |
ChecksumErrors | Int32 | Attributes whose 16-bit checksum did not match (content still used). |
Truncated | Boolean | True when the stream ended mid-attribute (truncated file); everything recovered up to that point is still returned. |
| Member | Type | Summary |
|---|---|---|
ToString() | String |
Bastion.Pst.Import
Reads a vCard (.vcf) file: yields each card as a contact-kind clsImportedMessage. Malformed cards are skipped; never throws on content.
| Member | Type | Summary |
|---|---|---|
Parse(String) | List(Of clsImportedMessage) | Parse vCard text: one imported contact per BEGIN:VCARD…END:VCARD block. |
ReadFile(String) | IEnumerable(Of clsImportedMessage) | Enumerate every vCard in a .vcf file as an imported contact. |
Bastion.Pst.Import
What kind of Outlook item an imported file parsed into. Mail is the default; Appointment/Contact are produced by the ICS and VCF readers (Import Phase 4); Task/Note are produced by the authoring API (Phase 2.3) — no interchange reader emits them.
| Member | Value |
|---|---|
Mail | 0 |
Appointment | 1 |
Contact | 2 |
Task | 3 |
Note | 4 |
Journal | 5 |
DistributionList | 6 |
Rss | 7 |
Bastion.Pst.Import
Days-of-week bitmask for a weekly recurrence (matches [MS-OXOCAL] PatternTypeWeek).
| Member | Value |
|---|---|
None | 0 |
Sunday | 1 |
Monday | 2 |
Tuesday | 4 |
Wednesday | 8 |
Thursday | 16 |
Friday | 32 |
Saturday | 64 |
Bastion.Pst.Import
Recurrence frequency for an authored appointment.
| Member | Value |
|---|---|
None | 0 |
Daily | 1 |
Weekly | 2 |
Monthly | 3 |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
AllAttendees | String | |
Attendees | IReadOnlyList(Of clsRecipient) | |
BusyStatus | enmBusyStatus | |
DurationMinutes | Int32 | |
EndTime | Nullable(Of DateTime) | |
IsAllDay | Boolean | |
IsRecurring | Boolean | |
Location | String | |
ReminderMinutesBeforeStart | Int32 | |
ReminderSet | Boolean | |
StartTime | Nullable(Of DateTime) |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
ContentId | String | Content id of an attachment referenced from the HTML body as cid:… — an inline image. A host rendering the HTML needs this to resolve those references, and a reading pane should list such an attachment in the body rather than as a paperclip. Empty for an ordinary attached file. |
FileName | String | Best file name: long name, then short (8.3) name, then display name. |
IsEmbeddedMessage | Boolean | |
Method | enmAttachMethod | |
MimeTag | String | |
Size | Int32 |
| Member | Type | Summary |
|---|---|---|
GetData() | Byte[] | The attachment's bytes for afByValue attachments (PidTagAttachDataBinary); Nothing for by-reference or embedded-message attachments. |
GetEmbeddedMessage() | clsMessage | The embedded message of an afEmbeddedMessage attachment, or Nothing. PidTagAttachDataObject holds an 8-byte {subnode NID, size} record ([MS-PST] 2.4.6.2); the message lives in that subnode of the attachment's own subtree, complete with its recipient/attachment tables (which may nest further embedded messages). |
ToString() | String |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
Anniversary | Nullable(Of DateTime) | |
Birthday | Nullable(Of DateTime) | |
BusinessAddress | String | |
BusinessFax | String | |
BusinessTelephone | String | |
CompanyName | String | |
Department | String | |
DisplayName | String | |
Email1Address | String | |
Email1DisplayName | String | |
Email2Address | String | |
Email3Address | String | |
FileUnder | String | |
GivenName | String | |
HomeAddress | String | |
HomeTelephone | String | |
JobTitle | String | |
MiddleName | String | |
MobileTelephone | String | |
Nickname | String | |
OfficeLocation | String | |
Surname | String | |
WebPage | String |
Bastion.Pst.Messaging
One member of a distribution list: who they are, and where mail to them goes.
| Member | Type | Summary |
|---|---|---|
Address | String | |
AddressType | String | |
DisplayName | String |
| Member | Type | Summary |
|---|---|---|
ToString() | String | What a list shows: the name, falling back to the address. |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
MemberCount | Int32 | Member count from PidLidDistributionListMembers — a PtypMultipleBinary whose value begins with a 4-byte element count ([MS-OXCDATA] 2.11.1.5). 0 if absent. |
Members | List(Of clsDistListMember) | Who is actually in the list. Without this a group is only ever a name and a number, which is why hosts drew it as an empty contact card — there was nothing else to show. Each member is a One-Off EntryID ([MS-OXCDATA] 2.2.5.1) inside the multi-valued binary; an unreadable element is skipped rather than taking the whole list down. |
Name | String |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
ContainerClass | String | The folder's container class (PidTagContainerClass) — "IPF.Note", "IPF.Appointment", "IPF.Contact", "IPF.Task", "IPF.StickyNote", "IPF.Journal", … — how a host app tells a calendar folder from a mail folder. Empty when unset (an unset class is conventionally treated as mail). |
ContentCount | Int32 | |
DisplayName | String | |
HasSubfolders | Boolean | |
UnreadableTable | Boolean | True when a table read for this folder failed and was tolerated — the folder reported fewer rows (possibly none) than it really holds. Tolerating a broken table keeps a damaged store walkable, but it makes "this folder is empty" and "this folder could not be read" look identical to a caller, which turns corruption into plausible-looking data. A host that lists folders should check this (or compare against ContentCount) and tell the user the folder is damaged rather than silently showing it as empty. |
UnreadCount | Int32 |
| Member | Type | Summary |
|---|---|---|
AssociatedMessages() | IEnumerable(Of clsMessage) | Folder-associated (FAI) messages, from the FAI Contents Table. |
Messages() | IEnumerable(Of clsMessage) | Messages, from the Contents Table (empty if none). Lazy — each opens on access. |
OpenMessage(UInt32) | clsMessage | Reopen a message in this store by its clsMessage.NodeId — the host pattern for resolving full detail (body, recipients, attachments) of a row it listed earlier without holding the whole message graph in memory. |
Search(clsMessageQuery) | IEnumerable(Of clsSearchHit) | Search this folder and its subfolders for messages matching query. |
SubFolders() | IEnumerable(Of clsFolder) | Subfolders, from the Hierarchy Table (empty if none). |
ToString() | String |
Bastion.Pst.Messaging
The standard PidTagContainerClass values — what tells a host application that a folder holds appointments rather than mail. Pass one of these to clsAuthor.SetFolderClass, or compare against clsFolder.ContainerClass.
| Member | Type | Summary |
|---|---|---|
Appointment | String | Calendar items — "IPF.Appointment". |
Birthday | String | The birthday calendar Outlook maintains — "IPF.Appointment.Birthday". |
Contact | String | Contacts — "IPF.Contact". |
Homepage | String | A folder homepage — "IPF.Note.OutlookHomepage". |
Journal | String | Journal entries — "IPF.Journal". |
Mail | String | Mail and post items — "IPF.Note". The default for a new folder. |
StickyNote | String | Sticky notes — "IPF.StickyNote". |
Task | String | Tasks — "IPF.Task". |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
Body | String | |
CreationTime | Nullable(Of DateTime) | |
Kind | enmItemKind | |
LastModificationTime | Nullable(Of DateTime) | |
Message | clsMessage | |
MessageClass | String | |
Subject | String |
| Member | Type | Summary |
|---|---|---|
Wrap(clsMessage) | itfItem | Wrap a message in the typed interface matching its PidTagMessageClass. Always returns a usable item; an unrecognised class is projected as a mail item (the MAPI base item class). |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
DurationMinutes | Int32 | |
EndTime | Nullable(Of DateTime) | |
JournalType | String | |
StartTime | Nullable(Of DateTime) |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
Attachments | IReadOnlyList(Of clsAttachment) | |
BodyHtml | String | The message's HTML body, or "" when it has none. PidTagBodyHtml is usually stored as BINARY, not as a string — so reading it as a string returned nothing for almost every HTML message ever sent, and hosts fell back to showing the plain-text alternative (a wall of bracketed URLs) as if the message had no HTML at all. The bytes are decoded with the message's own code page (PidTagInternetCodepage), falling back to the charset declared in the HTML itself, then to Windows-1252 — the encoding most untagged mail actually uses. |
ConversationTopic | String | |
DeliveryTime | Nullable(Of DateTime) | |
DisplayBcc | String | |
DisplayCc | String | |
DisplayTo | String | |
HasAttachments | Boolean | |
Importance | enmImportance | |
InternetMessageId | String | |
IsHtmlFormat | Boolean | True when this message is an HTML message. Checks the body form the message declares (PidTagNativeBody) and, failing that, whether an HTML body is actually present — a host should not have to fetch and measure the body just to decide how to render it. |
IsUnread | Boolean | |
Recipients | IReadOnlyList(Of clsRecipient) | |
SenderEmail | String | |
SenderName | String | |
Sensitivity | enmSensitivity | |
SubmitTime | Nullable(Of DateTime) |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
AppointmentEnd | Nullable(Of DateTime) | Appointment end (PidLidAppointmentEndWhole), for calendar items. |
AppointmentStart | Nullable(Of DateTime) | Appointment start (PidLidAppointmentStartWhole), for calendar items. |
Attachments | IReadOnlyList(Of clsAttachment) | The message attachments, from the Attachment Table subnode (empty if none). |
Body | String | |
ContactEmail | String | Primary email address (PidLidEmail1EmailAddress), for contact items. |
DeliveryTime | Nullable(Of DateTime) | |
HasAttachments | Boolean | |
Importance | Int32 | PidTagImportance (0=low, 1=normal, 2=high); 1 if unset. |
InternetMessageId | String | PidTagInternetMessageId. |
IsUnread | Boolean | True when the message is unread (MSGFLAG_READ not set). |
Kind | enmItemKind | |
Location | String | Appointment / meeting location (PidLidLocation). |
MessageClass | String | |
MessageFlags | Int32 | PidTagMessageFlags. |
NodeId | UInt32 | The message's stable node identifier within its open store — the value a host application uses to correlate a list row with the message and to reopen it for detail (see clsFolder.OpenMessage). Stable for the lifetime of the open store. |
Recipients | IReadOnlyList(Of clsRecipient) | The message recipients (To/Cc/Bcc), from the Recipient Table subnode. |
SenderEmail | String | |
SenderName | String | |
Size | Int32 | PidTagMessageSize. |
Subject | String | The subject, with any stored prefix marker removed. |
SubmitTime | Nullable(Of DateTime) |
| Member | Type | Summary |
|---|---|---|
AsItem() | itfItem | Project this message as the typed item interface matching its message class (mail, appointment, contact, task, note, journal, distribution list). Read-only and allocation-cheap — wrap, read, discard, per message. |
ToString() | String |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
HasClass(String) | clsMessageQuery | Message class starts with prefix (e.g. "IPM.Note"). |
HasMessageId(String) | clsMessageQuery | |
ImportanceAtLeast(Int32) | clsMessageQuery | |
LargerThan(Int32) | clsMessageQuery | |
Matches(clsMessage) | Boolean | True when the message satisfies every criterion (empty query matches all). |
OfKind(enmItemKind) | clsMessageQuery | |
RecipientContains(String) | clsMessageQuery | |
SenderContains(String) | clsMessageQuery | |
SubjectContains(String) | clsMessageQuery | |
Unread() | clsMessageQuery | |
Where(Func(Of clsMessage, Boolean)) | clsMessageQuery | Add an arbitrary criterion. |
WithAttachments(Boolean) | clsMessageQuery |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
Color | Int32 | |
Height | Int32 | |
Width | Int32 |
Bastion.Pst.Messaging
Result of clsPersonalStorage.TryOpen: ErrorCode 0 = opened, Store usable.
| Member | Type | Summary |
|---|---|---|
Store | clsPersonalStorage | The opened store (Nothing on failure). Dispose it when done. |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
DisplayName | String | The store's display name (PidTagDisplayName on the message store). |
Format | enmPstFormat | |
HasPassword | Boolean | True when the file has an open password set (PidTagPstPassword present and non-zero on the message store). A PST password is a stored CRC hash, not encryption — the library reads the file regardless. Set or clear it with Convert.clsStorePassword.SetPassword / RemovePassword. |
IsUnicode | Boolean | |
RootFolder | clsFolder | The root Folder object (NID 0x122). Its subfolders include the IPM subtree. |
SourcePath | String | The file this store was opened from, or "" when it was opened from a stream. A host with several stores open needs to know which one an item came from, and every row the view layer produces carries it. |
SupportsWriting | Boolean | Always False for now — this build reads but does not write. |
Views | clsStoreViews | The store's sections — mail, calendar, contacts, tasks, notes — as bindable rows: store.Views.Contacts(), store.Views.Mail(query). This is what a host binds a grid or a card view to; see clsItemQuery for filtering that avoids opening items it can rule out from the folder's contents table. |
| Member | Type | Summary |
|---|---|---|
Dispose() | Void | |
IsAnsi(String) | Boolean | True if strPath is a legacy ANSI PST — i.e. adding to or editing it would upgrade it to Unicode (see the AnsiUpgradeRequired gate). Cheap header-only check. |
Open(Stream, Boolean) | clsPersonalStorage | Open a PST/OST file for reading. |
Open(String) | clsPersonalStorage | Open a PST/OST file for reading. |
PeekFormat(String) | enmPstFormat | Cheap pre-flight: report a file's PST format by reading ONLY its header (no full parse, no lock held). Returns enmPstFormat.Ansi / enmPstFormat.Unicode, or enmPstFormat.Unknown if the file is missing, unreadable, or not a PST/OST. Use before a write to warn that adding to an ANSI store will upgrade it to Unicode. |
Search(clsMessageQuery) | IEnumerable(Of clsSearchHit) | Search the whole store for messages matching query. |
TryOpen(String) | clsOpenResult | Open a PST/OST for reading WITHOUT throwing: the result carries ErrorCode (0 = opened and .Store usable; otherwise FileNotFound / FileLocked (mounted in Outlook?) / InvalidPstFormat / … with a human-readable ErrorDescription). Dispose the Store when done. |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
Address | String | The best available address for display (SMTP if present, else the email address). |
AddressType | String | |
DisplayName | String | |
EmailAddress | String | |
RecipientType | enmRecipientType | |
SmtpAddress | String |
| Member | Type | Summary |
|---|---|---|
ToString() | String |
Bastion.Pst.Messaging
Walks a folder subtree applying a clsMessageQuery.
| Member | Type | Summary |
|---|---|---|
Find(clsPersonalStorage, clsMessageQuery) | IEnumerable(Of clsSearchHit) | Search the whole store. |
Walk(clsFolder, String, clsMessageQuery) | IEnumerable(Of clsSearchHit) | Search a folder subtree. |
Bastion.Pst.Messaging
A search result: the message and the folder path it was found in.
| Member | Type | Summary |
|---|---|---|
FolderPath | String | |
Message | clsMessage |
Bastion.Pst.Messaging
| Member | Type | Summary |
|---|---|---|
DateCompleted | Nullable(Of DateTime) | |
DueDate | Nullable(Of DateTime) | |
Importance | enmImportance | |
IsComplete | Boolean | |
Owner | String | |
PercentComplete | Double | |
StartDate | Nullable(Of DateTime) | |
Status | enmTaskStatus |
Bastion.Pst.Messaging
PidTagAttachMethod (0x3705) — how an attachment's data is stored.
| Member | Value |
|---|---|
None | 0 |
ByValue | 1 |
ByReference | 2 |
ByReferenceResolve | 3 |
ByReferenceOnly | 4 |
EmbeddedMessage | 5 |
Storage | 6 |
Bastion.Pst.Messaging
PidLidBusyStatus (PSETID_Appointment 0x8205) — free/busy for an appointment.
| Member | Value |
|---|---|
Free | 0 |
Tentative | 1 |
Busy | 2 |
OutOfOffice | 3 |
WorkingElsewhere | 4 |
Bastion.Pst.Messaging
PidTagImportance (0x0017).
| Member | Value |
|---|---|
Low | 0 |
Normal | 1 |
High | 2 |
Bastion.Pst.Messaging
The kind of MAPI item, derived from PidTagMessageClass.
| Member | Value |
|---|---|
Unknown | 0 |
Mail | 1 |
Appointment | 2 |
Contact | 3 |
Task | 4 |
Note | 5 |
Journal | 6 |
DistributionList | 7 |
Bastion.Pst.Messaging
PidTagRecipientType (0x0C15) — the role of a recipient on a message.
| Member | Value |
|---|---|
Originator | 0 |
To | 1 |
Cc | 2 |
Bcc | 3 |
Bastion.Pst.Messaging
PidTagSensitivity (0x0036).
| Member | Value |
|---|---|
Normal | 0 |
Personal | 1 |
Private2 | 2 |
Confidential | 3 |
Bastion.Pst.Messaging
PidLidTaskStatus (PSETID_Task 0x8101).
| Member | Value |
|---|---|
NotStarted | 0 |
InProgress | 1 |
Complete | 2 |
Waiting | 3 |
Deferred | 4 |
Bastion.Pst.Messaging
Appointment / meeting — [MS-OXOCAL].
| Member | Type | Summary |
|---|---|---|
AllAttendees | String | |
Attendees | IReadOnlyList(Of clsRecipient) | Meeting attendees (the message recipients). |
BusyStatus | enmBusyStatus | |
DurationMinutes | Int32 | |
EndTime | Nullable(Of DateTime) | |
IsAllDay | Boolean | |
IsRecurring | Boolean | |
Location | String | |
ReminderMinutesBeforeStart | Int32 | |
ReminderSet | Boolean | |
StartTime | Nullable(Of DateTime) |
Bastion.Pst.Messaging
Contact — [MS-OXOCNTC].
| Member | Type | Summary |
|---|---|---|
Anniversary | Nullable(Of DateTime) | |
Birthday | Nullable(Of DateTime) | |
BusinessAddress | String | Composed mailing (business) address. |
BusinessFax | String | |
BusinessTelephone | String | |
CompanyName | String | |
Department | String | |
DisplayName | String | |
Email1Address | String | |
Email1DisplayName | String | |
Email2Address | String | |
Email3Address | String | |
FileUnder | String | |
GivenName | String | |
HomeAddress | String | Composed home address. |
HomeTelephone | String | |
JobTitle | String | |
MiddleName | String | |
MobileTelephone | String | |
Nickname | String | |
OfficeLocation | String | |
Surname | String | |
WebPage | String |
Bastion.Pst.Messaging
Distribution list — [MS-OXOABK] / [MS-OXODLGT].
| Member | Type | Summary |
|---|---|---|
MemberCount | Int32 | Number of members (count from PidLidDistributionListMembers); 0 if absent. |
Members | List(Of clsDistListMember) | The members themselves — name and address apiece. Empty when the list carries no readable member property. |
Name | String |
Bastion.Pst.Messaging
Common projection shared by every typed item.
| Member | Type | Summary |
|---|---|---|
Body | String | Plain-text body (PidTagBody). |
CreationTime | Nullable(Of DateTime) | |
Kind | enmItemKind | Item kind from PidTagMessageClass. |
LastModificationTime | Nullable(Of DateTime) | |
Message | clsMessage | The underlying raw message (escape hatch to the full PC / named props). |
MessageClass | String | PidTagMessageClass verbatim (e.g. "IPM.Appointment"). |
Subject | String |
Bastion.Pst.Messaging
Journal entry — [MS-OXOJRNL].
| Member | Type | Summary |
|---|---|---|
DurationMinutes | Int32 | |
EndTime | Nullable(Of DateTime) | |
JournalType | String | |
StartTime | Nullable(Of DateTime) |
Bastion.Pst.Messaging
Mail message — [MS-OXOMSG].
| Member | Type | Summary |
|---|---|---|
Attachments | IReadOnlyList(Of clsAttachment) | |
BodyHtml | String | |
ConversationTopic | String | |
DeliveryTime | Nullable(Of DateTime) | |
DisplayBcc | String | |
DisplayCc | String | |
DisplayTo | String | |
HasAttachments | Boolean | |
Importance | enmImportance | |
InternetMessageId | String | |
IsHtmlFormat | Boolean | True when the message was written as HTML — i.e. it should be rendered as HTML rather than shown as text. False for a plain-text message. |
IsUnread | Boolean | |
Recipients | IReadOnlyList(Of clsRecipient) | |
SenderEmail | String | |
SenderName | String | |
Sensitivity | enmSensitivity | |
SubmitTime | Nullable(Of DateTime) |
Bastion.Pst.Messaging
Sticky note — [MS-OXONOTE]. The note text is itfItem.Body.
| Member | Type | Summary |
|---|---|---|
Color | Int32 | PidLidNoteColor (0=blue,1=green,2=pink,3=yellow,4=white). |
Height | Int32 | |
Width | Int32 |
Bastion.Pst.Messaging
Task — [MS-OXOTASK].
| Member | Type | Summary |
|---|---|---|
DateCompleted | Nullable(Of DateTime) | |
DueDate | Nullable(Of DateTime) | |
Importance | enmImportance | |
IsComplete | Boolean | |
Owner | String | |
PercentComplete | Double | Fraction complete, 0.0 .. 1.0. |
StartDate | Nullable(Of DateTime) | |
Status | enmTaskStatus |
Bastion.Pst.Ndb
On-disk PST/OST format variant. The NDB layer is the only layer that distinguishes these; everything above it is format-agnostic ([MS-PST] 2.2.2.6).
| Member | Value |
|---|---|
Unknown | 0 |
Ansi | 1 |
Unicode | 2 |
Bastion.Pst.Repair
| Member | Type | Summary |
|---|---|---|
Repair(String, String, clsDiagnostics, Boolean, Boolean, Int64) | clsRepairReport | Repair srcPath into a fresh, structurally-canonical Unicode PST at outPath. Pass a diag with clsDiagnostics.RecoverFromCorruption set to skip individual nodes that fail to read/serialise instead of aborting. Returns before/after validation and the node count. |
Bastion.Pst.Repair
Outcome of a clsPstRepair.Repair: the node count plus the strict validation of the source (before) and the rebuilt output (after).
| Member | Type | Summary |
|---|---|---|
NodesWritten | Int32 | |
OrphansRecovered | Int32 | Messages that had lost their folder and were re-homed into a "Lost and Found" folder. |
OutputIsValid | Boolean | True when the repaired output passes strict structural validation. |
OutputPath | String | |
OutputReport | clsValidationReport | Validation of the rebuilt output. |
PasswordStripped | Boolean | True if the password property was cleared on the output. |
SourcePath | String | |
SourceReport | clsValidationReport | Validation of the source as supplied (Nothing if it could not be validated at all). |
SourceWasValid | Boolean | |
SubtreeReconnected | Boolean | True when the mailbox's folder tree was reconnected to the store root (its root link had been destroyed, orphaning every folder). |
Succeeded | Boolean | Whether the repair completed (ErrorCode 0) AND produced an Outlook-acceptable file. |
Bastion.Pst.Validation
Outcome of clsPstHealth.Check. ErrorCode is 0 when the check itself ran — a corrupt-but-examinable file is ErrorCode 0 with IsCorrupted=True; ErrorCode is non-zero only when the file could not be examined at all.
| Member | Type | Summary |
|---|---|---|
ErrorCount | Int32 | |
FatalError | String | Set when validation itself failed hard (unreadable header etc.). |
IsCorrupted | Boolean | True when the file has structural errors (or could not be read at all). |
Path | String | |
Validation | clsValidationReport | The full validator report (Nothing when the file could not even be opened). |
| Member | Type | Summary |
|---|---|---|
RepairTo(String, clsDiagnostics) | clsRepairReport | Repair this file to outPath (recovery mode: damaged items are skipped, everything readable is rebuilt into a structurally-canonical PST). |
SaveSupportReport(String, String, String) | clsFileOpResult | Write the developer support report (structural metadata only — no message content). outPath names the target file; when empty the report goes to the user's temp folder (or directory when supplied) as BastionPstSdk-SupportReport-<file>-<stamp>.txt. NEVER throws: check the returned result's ErrorCode (0 = written, .Path = where). |
Bastion.Pst.Validation
One-call health check for a PST/OST: full structural validation surfaced as a simple corrupted/clean verdict, plus support-report generation and a repair shortcut.
| Member | Type | Summary |
|---|---|---|
Check(String, clsDiagnostics, Boolean) | clsHealthResult | Validate path structurally (pages, blocks, CRCs, B-trees, maps). Never throws on a corrupt file — an unreadable file reports as corrupted with the failure captured as an issue. When diag is supplied, corruption is also surfaced through it (event + logs + auto support report per its settings). |
Bastion.Pst.Validation
| Member | Type | Summary |
|---|---|---|
Validate(Stream) | clsValidationReport | Validate the PST at path; never throws — a malformed file, and a missing / locked / unreadable one, are reported as issues (IsValid = False), not exceptions. |
Validate(String) | clsValidationReport | Validate the PST at path; never throws — a malformed file, and a missing / locked / unreadable one, are reported as issues (IsValid = False), not exceptions. |
Bastion.Pst.Validation
One structural problem found by clsPstValidator.
| Member | Type | Summary |
|---|---|---|
Category | String | |
Message | String | |
Offset | UInt64 | |
Severity | enmValidationSeverity |
| Member | Type | Summary |
|---|---|---|
ToString() | String |
Bastion.Pst.Validation
Result of validating a PST: the issues found and how much was checked.
| Member | Type | Summary |
|---|---|---|
BlocksChecked | Int32 | |
ErrorCount | Int32 | |
Issues | IReadOnlyList(Of clsValidationIssue) | |
IsValid | Boolean | True when no errors were found (warnings do not fail the gate). |
PagesChecked | Int32 | |
WarningCount | Int32 |
Bastion.Pst.Validation
| Member | Value |
|---|---|
Error | 0 |
Warning | 1 |
Bastion.Pst.Views
An appointment as a calendar shows it.
| Member | Type | Summary |
|---|---|---|
Attendees | String | |
Body | String | |
BusyStatus | enmBusyStatus | |
Display | String | |
EndTime | Nullable(Of DateTime) | |
IsAllDay | Boolean | |
IsRecurring | Boolean | |
Location | String | |
OccurrenceKey | String | Identity for spotting the same appointment archived twice: same subject, same start, same length. Used when merging calendars of the same name. |
Organiser | String | |
ReminderSet | Boolean | |
StartTime | Nullable(Of DateTime) | |
Subject | String |
Bastion.Pst.Views
One attachment, as a reading pane lists it.
| Member | Type | Summary |
|---|---|---|
ContentId | String | |
FileName | String | |
Index | Int32 | |
IsEmbeddedMessage | Boolean | |
IsInline | Boolean | True when the attachment is displayed inside the HTML body rather than listed (an inline image); a reading pane should not show it as a paperclip. |
MimeTag | String | |
SizeBytes | Int32 |
Bastion.Pst.Views
| Member | Type | Summary |
|---|---|---|
Combine(IEnumerable(Of clsContactView)) | List(Of clsContactView) | Combine contact rows that are the same person. Order is preserved: the first entry seen for a person is the one kept and filled in from the rest. The rows passed in are never modified. Folding is done into a copy, so a caller may hold its projected contacts and merge them again — which a host does on every folder or option change — without the second merge folding already-folded rows into themselves and running the "combined from N entries" count away with itself. |
CombineByEmail(IEnumerable(Of clsContactView)) | List(Of clsContactView) | Combine contacts that share an e-mail address, whatever their names say. Use with care and never silently: a shared address is not always one person — info@, support@ and a family address are all held by several people, and merging those would fuse unrelated contacts into one. Matching on name (Combine) is the safe default; this is the opt-in. As with Combine, the rows passed in are never modified. |
Bastion.Pst.Views
A contact as a card view shows it.
| Member | Type | Summary |
|---|---|---|
AllEmails | IEnumerable(Of String) | Every e-mail address on the contact, primary first, blanks removed. |
AlternateEmails | List(Of String) | |
AlternatePhones | List(Of String) | |
Anniversary | Nullable(Of DateTime) | |
Birthday | Nullable(Of DateTime) | |
BusinessAddress | String | |
BusinessFax | String | |
BusinessPhone | String | |
Company | String | |
Department | String | |
Display | String | |
Email | String | The address to show when there is room for one. |
Email1 | String | |
Email2 | String | |
Email3 | String | |
FileUnder | String | |
FullName | String | |
GivenName | String | |
HomeAddress | String | |
HomePhone | String | |
IsDistributionList | Boolean | True when this row is a distribution list (a contact GROUP), not a person. An archive's contacts folder holds both, and a group has none of a person's fields — so a host that does not check this draws a card with every line blank. |
JobTitle | String | |
MemberNames | String | The members as one line, for a card that has room for a line and not a list. |
Members | List(Of clsDistListMember) | Members of the group, when IsDistributionList is set. |
MembershipSummary | String | What a group has in place of a job title: how many people are in it. Empty for a person, so a host can bind one label to it either way. |
MergedFrom | Int32 | |
MergedFromMemberCount | Int32 | Member count as the list itself reported it, used when the members could not be decoded but the count could. |
MobilePhone | String | |
Notes | String | |
Phone | String | The number to show when there is room for one: business, then mobile, then home — the order Outlook prefers on a card. |
Photo | Byte[] | The contact's photo (JPEG/PNG bytes) if the card carries one, else Nothing. |
SortKey | String | Surname-first key Outlook files contacts under. A group has no surname, so it files under its own name. |
SourceFolders | List(Of String) | |
Surname | String | |
WebPage | String |
| Member | Type | Summary |
|---|---|---|
Clone() | clsContactView | An independent copy of this row. clsContactMerger folds several entries into one by writing into the row it keeps, so it clones first — otherwise it would edit the caller's rows, and a host that holds its projected contacts in memory and re-merges them (on a folder change, say) would fold already-folded rows into themselves over and over.The lists are new lists over the same strings, so appending to a copy's AlternateEmails cannot reach the original. Photo and Members are shared by reference: both are read-only once projected, and copying a photo per merge would be pure waste. |
Bastion.Pst.Views
The full detail of one item, for a reading pane or an item window.
| Member | Type | Summary |
|---|---|---|
Attachments | List(Of clsAttachmentView) | |
BodyHtml | String | The HTML body, or "" when the message has none. |
BodyPlain | String | The plain-text body, or "" — always populated when there is any body at all, so a host with no HTML renderer still has something to show. |
Date | Nullable(Of DateTime) | |
DisplayBcc | String | |
DisplayCc | String | |
DisplayTo | String | |
Error | String | Set when the item could not be read at all; the host shows this instead of a confidently blank message. |
HasHtml | Boolean | True when BodyHtml is real HTML from the message rather than a fallback. A host should render HTML when this is set and plain text otherwise. |
IsHtmlFormat | Boolean | The same fact said the way an e-mail object says it: this message is HTML format. |
Kind | enmItemKind | |
MessageClass | String | |
Nid | UInt32 | |
Recipients | List(Of clsRecipient) | |
SenderEmail | String | |
SenderName | String | |
Subject | String |
Bastion.Pst.Views
| Member | Type | Summary |
|---|---|---|
CopyItems(String, String, IEnumerable(Of UInt32), String, String, String, clsDiagnostics) | clsEditReport | Copy items from one store into a folder of another (or the same) store. Each item is exported to a native .msg and imported into the target, so every kind copies with full fidelity — recipients, attachments and MAPI properties — and the source is only ever read. Returns the rewritten target as a temporary file the caller swaps in. |
DeleteItems(String, String, String, ISet(Of UInt32), clsDiagnostics) | clsEditReport | Delete items from a folder, writing the result to outPath. |
ImportFiles(String, String, String, IEnumerable(Of String), clsDiagnostics) | clsEditReport | Import files (.msg .eml .ics .vcf .mbox …) into a folder. |
IsWritable(String) | Boolean | |
MoveItems(String, String, String, String, ISet(Of UInt32), clsDiagnostics) | clsEditReport | Move items between folders of the same store. |
SaveItems(String, String, IEnumerable(Of UInt32), String, enmSaveFormat, CancellationToken, Action(Of Int32)) | clsSaveReport | Save items to disk in a format other applications can open: mail as .msg, contacts as .vcf, appointments as .ics, notes as .txt — or force one format for the lot. The source is opened read-only and never changed. |
WhyNotWritable(String) | String | Why a store will not accept data, or "" when it will. A host should show this rather than inventing its own wording. |
Bastion.Pst.Views
| Member | Type | Summary |
|---|---|---|
BodyContains(String) | clsItemQuery | Body contains text. Expensive: opens every surviving item. |
DateBefore(DateTime) | clsItemQuery | Item dated before until. Cheap. |
DateFrom(DateTime) | clsItemQuery | Item dated on or after from. Cheap. |
HasClass(String) | clsItemQuery | Message class starts with prefix ("IPM.Contact"). Cheap. |
InFolder(String) | clsItemQuery | Only look in this folder path (exact, case-insensitive). |
InFolders(Func(Of String, Boolean)) | clsItemQuery | Only look in folders whose full path satisfies predicate — the cheapest filter of all, since a folder that fails is never opened. |
LargerThan(Int32) | clsItemQuery | Item is larger than bytes. Cheap. |
SenderContains(String) | clsItemQuery | Sender name or address contains text. Cheap. |
SubjectContains(String) | clsItemQuery | Subject contains text. Cheap — read from the table row. |
Take(Int32) | clsItemQuery | Stop after n matches (0 = no limit). |
TextContains(String) | clsItemQuery | Subject OR sender contains text — what a search box means. Cheap. |
Unread() | clsItemQuery | Unread only. Cheap. |
Where(Func(Of clsMessage, Boolean)) | clsItemQuery | An arbitrary test against the opened item. Runs only for rows that passed every cheap criterion, so put anything the contents table can answer in the criteria above. |
WithAttachments() | clsItemQuery | With attachments only. Cheap. |
Bastion.Pst.Views
Fields shared by every kind of item.
| Member | Type | Summary |
|---|---|---|
Display | String | What a list shows when it has one line to spare. |
FolderName | String | Display name of the folder holding the item ("Inbox"). |
FolderNid | UInt32 | Node id of that folder — lets a host act on the item without re-walking the tree. |
FolderPath | String | Full path of the folder holding the item ("Top of Outlook data file\Inbox"). |
Kind | enmItemKind | |
MessageClass | String | |
Nid | UInt32 | Node id of the item within its store — the handle for reopening it. |
Size | Int32 | |
StorePath | String | File path of the store the item came from; a host may have several open. |
Bastion.Pst.Views
A message as a mail list shows it.
| Member | Type | Summary |
|---|---|---|
Date | Nullable(Of DateTime) | Received if known, else sent — what Outlook sorts a mail list by. |
Display | String | |
FromDisplay | String | Who the row is from: the sender's name, their address if unnamed. |
HasAttachments | Boolean | |
Importance | Int32 | |
IsHtmlFormat | Boolean | True when the message was written as HTML and should be rendered as HTML. |
IsUnread | Boolean | |
Preview | String | First line or so of the body, for the second/third line of a message list. |
Received | Nullable(Of DateTime) | |
SenderEmail | String | |
SenderName | String | |
Sent | Nullable(Of DateTime) | |
SizeBytes | Int32 | Size in bytes — the name a size column usually binds to. |
Subject | String | |
To | String |
Bastion.Pst.Views
A sticky note.
| Member | Type | Summary |
|---|---|---|
Body | String | |
Colour | Int32 | |
Created | Nullable(Of DateTime) | |
Display | String | |
Subject | String |
Bastion.Pst.Views
| Member | Type | Summary |
|---|---|---|
Date | Nullable(Of DateTime) | |
DisplayTo | String | |
HasAttachments | Boolean | |
Importance | Int32 | |
IsUnread | Boolean | |
MessageClass | String | |
Nid | UInt32 | |
Received | Nullable(Of DateTime) | |
SenderEmail | String | |
SenderName | String | |
Sent | Nullable(Of DateTime) | |
Size | Int32 | |
Subject | String |
Bastion.Pst.Views
An RSS feed post. Outlook files these under "RSS Feeds" with the container class IPF.Note.OutlookHomepage and the item class IPM.Post.Rss - close enough to mail to be mistaken for it, which is why they used to appear in the mail folder tree, but they are their own thing: a headline, a feed, a publication date and a link back to the article.
| Member | Type | Summary |
|---|---|---|
Author | String | |
Display | String | |
FeedName | String | The feed the post came from - normally the folder it is filed in. |
IsHtmlFormat | Boolean | |
Link | String | Link back to the original article, when the post carries one. |
Preview | String | |
Published | Nullable(Of DateTime) | |
Subject | String |
Bastion.Pst.Views
What a save/export produced.
| Member | Type | Summary |
|---|---|---|
Error | String | |
Failed | Int32 | |
Files | List(Of String) | |
Ok | Boolean | |
Saved | Int32 |
Bastion.Pst.Views
A folder belonging to a section — a calendar, a contact folder — for a host to list.
| Member | Type | Summary |
|---|---|---|
FullPath | String | |
ItemCount | Int32 | |
Name | String | |
Nid | UInt32 | |
StorePath | String |
Bastion.Pst.Views
| Member | Type | Summary |
|---|---|---|
EmptyEmailPlaceholder | String | Shown in place of a missing sender address (e.g. "empty@myaddress.com"). Purely a display substitution — the stored item is never changed. Empty shows nothing. |
HiddenFolderNames | HashSet(Of String) | Folders Outlook keeps out of sight. They carry a contact container class but hold address-book plumbing rather than people — in a real archive they outnumbered the genuine contacts ten to one. |
IncludeHiddenFolders | Boolean | Include Outlook's hidden address-book folders in contact views (default False). |
| Member | Type | Summary |
|---|---|---|
Appointments(clsItemQuery) | IEnumerable(Of clsAppointmentView) | Every appointment in the store. |
AttachmentBytes(clsFolder, UInt32, Int32) | Byte[] | The bytes of one attachment, by its index in clsItemDetail.Attachments. A list row carries no payload — a mail list must not hold every attachment in memory — so a host asks for the bytes when the user actually saves or opens one. |
Contacts(clsItemQuery) | IEnumerable(Of clsContactView) | Every contact in the store. |
Detail(clsFolder, UInt32) | clsItemDetail | Everything needed to display one item: body (HTML when it has one), recipients and attachments. Read one item at a time, when the user selects it — list rows stay thin. |
Mail(clsItemQuery) | IEnumerable(Of clsMailView) | Every mail item in the store. |
MailIn(clsFolder, clsItemQuery) | IEnumerable(Of clsMailView) | The mail in ONE folder — what a mail list binds to when the user clicks a folder. Reads the folder's contents table and nothing else, so a 23,000-message Inbox costs one table read rather than 23,000 message opens. |
Notes(clsItemQuery) | IEnumerable(Of clsNoteView) | Every note in the store. |
Rss(clsItemQuery) | IEnumerable(Of clsRssView) | Every RSS post in the store. |
SectionFolders(enmSection) | List(Of clsSectionFolder) | The folders that make up a section — the calendars, the contact folders — so a host can offer them as a list to switch between. |
Tasks(clsItemQuery) | IEnumerable(Of clsTaskView) | Every task in the store. |
Bastion.Pst.Views
A task as a to-do list shows it.
| Member | Type | Summary |
|---|---|---|
Body | String | |
DateCompleted | Nullable(Of DateTime) | |
Display | String | |
DueDate | Nullable(Of DateTime) | |
Importance | enmImportance | |
IsComplete | Boolean | |
IsOverdue | Boolean | |
Owner | String | |
PercentComplete | Double | |
StartDate | Nullable(Of DateTime) | |
Status | enmTaskStatus | |
StatusText | String | |
Subject | String |
Bastion.Pst.Views
The file an item is saved as when the user picks "Save As" with no format chosen.
| Member | Value |
|---|---|
Native | 0 |
Msg | 1 |
Vcf | 2 |
Ics | 3 |
Txt | 4 |
Bastion.Pst.Views
The sections of a mailbox.
| Member | Value |
|---|---|
Mail | 0 |
Calendar | 1 |
Contacts | 2 |
Tasks | 3 |
Notes | 4 |
Journal | 5 |
Rss | 6 |