Skip to content

Add PST → Thunderbird mail export - #80

Open
Aksel-Visby wants to merge 36 commits into
pst-to-thunderbird-integrationfrom
feat/pst-to-thunderbird
Open

Add PST → Thunderbird mail export#80
Aksel-Visby wants to merge 36 commits into
pst-to-thunderbird-integrationfrom
feat/pst-to-thunderbird

Conversation

@Aksel-Visby

Copy link
Copy Markdown
Member

What

Adds a reverse conversion path: turn an Outlook PST into a Thunderbird-importable
mbox folder tree, without Outlook or any Microsoft library. This complements the
existing forward mbox → PST converter, making ContinuMail a two-way bridge for mail.

New CLI subcommand:

continumail-convert export --input <path.pst> --output <dir> [--include-empty]

It reads the PST, reconstructs each message as MIME, and writes an mbox tree
(Name.sbd/ nesting mirroring the folder hierarchy) with Thunderbird's
X-Mozilla-Status / -Status2 / -Keys headers so Thunderbird regenerates its
own index on import.

How it works

  • Streaming reader — walks the PST one message at a time (a large store is never
    fully materialized); recovers subject, sender (name + address), recipients with
    To/Cc/Bcc kind, dates, Message-ID/In-Reply-To/References, plain and HTML bodies
    (with the stored code page), read/replied/forwarded state, categories, and
    attachments (including inline Content-ID and Content-Location).
  • Header mapping — flags and categories become X-Mozilla-Status/-Status2 bits
    and X-Mozilla-Keys tags (the inverse of how the forward parser reads them). The
    "flagged" state maps to the Marked bit.
  • MIME reconstruction — rebuilds a MimeKit message from the decoded properties,
    regenerating MIME-structural headers from the fresh body/attachment tree; legacy
    code pages decode via the CodePages provider with a UTF-8 fallback.
  • Mbox tree writer — writes the .sbd layout with mboxrd From escaping and the
    raw X-Mozilla-* header lines.
  • Export runner — orchestrates the pipeline, writes into a staging directory and
    publishes on success (an existing non-empty destination is rejected; a failure
    cleans up without leaving a half-written tree), and produces an export report
    (messages exported, folders, structured skips, warnings, output files).
  • CLI events — streams JSON Lines (started/progress/warning/skipped/done/
    error), each tagged with command: "export"; the report is written to sibling
    <output>.export-report.json / .txt files.

Vendored code

vendor/PSTFileFormat (LGPLv3) gains three additive PropertyID enum members
(PidTagTransportMessageHeaders, PidTagInternetReferences, PidTagInReplyToId) —
read-only additions, no behaviour change.

Testing

Full solution green. Round-trip coverage proves that a forward-written PST exports
back to an mbox tree preserving folder structure, per-folder counts, message bodies,
attachment bytes, and read/starred/tagged state. A manual Thunderbird-import check
remains a release-time validation step.

Known limitations / follow-ups

  • Thunderbird tag definitions are not created — tags are written as X-Mozilla-Keys
    values, but the visible tag name/colour depends on the importing profile's tag
    configuration, so a tag key may not render as a coloured label in a fresh profile.
  • A message that has no Message-ID at all currently gets a non-deterministic generated
    one (only reachable with arbitrary Outlook-authored PSTs).
  • Message importance/priority is not yet reconstructed.

The forward writer stores In-Reply-To and References as discrete MAPI
properties (0x1042 / 0x1039) rather than only inside the transport
headers blob. The reader now surfaces both on PstMailMessage, needed to
reconstruct threading on the engine-PST path where transport headers
are absent.
EnumerateFolder read mf.MessageCount (loop bound) and folder.MessageCount
outside the try that guards folder access, so a corrupt contents table
there propagated fatal instead of the stated warn+skip-subtree policy.
Both reads are now guarded, falling back to zero (skip this folder's
messages, continue the child walk) on failure.

Also stop the recipient-type warning closure from capturing the
nullable raw int32; it now threads the resolved value through
explicitly.
EnumerateMessages yields only message-bearing folders, so the mbox-tree
writer's full-structure requirement (empty leaves, structural parents)
cannot be derived from the message stream. Add a companion walk that
returns every mail folder path including empty ones, recursing through
non-mail containers and warning on a corrupt subtree.
… export

Folder-tree-oriented report (output root, mbox files, message/folder
counts, structured per-message skips kept separate from warnings, elapsed)
with ToJson/ToSummary, distinct from the forward ConversionReport.
Extract ReportMessageReadFailure: with an onSkipped sink it records a
structured skip (folder, index, reason) ONLY - no duplicate warning; without
one it emits the legacy warning. EnumerateMessages gains the optional
onSkipped param. Additive/non-breaking.
…ation

Two non-mutating passes over the PST (structure, then a streamed message
write) into a sibling staging dir, published to the final output via a
directory move as the last step; a non-empty/file destination is rejected
up front and a mid-export failure cleans staging. Skips and warnings are
kept distinct; per-message progress fires only after a successful append
(MboxTreeWriter gains an optional onMessageWritten callback).
…, trailing sep, staging cleanup, invalid PST)
Converts a nested profile (Parent/Inbox, Sent, Empty) to PST, exports it
back to an mbox tree, and re-parses with MboxParser asserting nested
relative paths, per-folder counts, zero skips, and per-message fields.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant