Files
lasuite-messages/docs/imports-pst.md
Sylvain Zimmerandjbpenrath 732b6028d3 ✨(imports) refactor import code: retries, continuous mode, list UI (#742)
Largely improves user & operator visibility on imports, resiliency with retries,
direct-to-offload storage, import cancellation (deleting imported messages) & more.
Remove the modal import and manage all within the import tab in the settings modal
2026-07-22 16:11:18 +02:00

9.6 KiB

PST Import

This document describes how PST (Personal Storage Table) files are imported into the application.

Overview

PST files are the native file format for Microsoft Outlook mailboxes. The import pipeline reads messages from a PST file stored in S3, converts them to RFC 5322 (EML) format, and delivers them through the standard inbound message pipeline with is_import=True.

Entry point: the run_pst runner in core/services/importer/pst.py (dispatched by the resumable run_import_task in core/services/importer/tasks.py; see imports.md).

Architecture

S3 (message-imports bucket)
  │
  ▼
S3SeekableReader (block-aligned LRU cache)
  │
  ▼
pypff (libpff) ── reads PST B-tree structures
  │
  ▼
pst.py ── folder detection, message extraction, EML reconstruction
  │
  ▼
pst.run_pst ── runner: flag/label mapping, progress reporting, resume
  │
  ▼
deliver_inbound_message(is_import=True)

S3 Seekable Reader

PST files are read directly from S3 without downloading to disk. The S3SeekableReader class (core/services/s3_seekable.py) implements a seekable file-like object backed by S3 range requests.

For PST files, the reader uses BUFFER_NONE strategy with a block-aligned LRU cache — 64 KB blocks with up to 2048 cache slots (128 MB max). This matches pypff's random-access pattern when traversing PST B-tree structures.

Folder Detection

PST files contain a hierarchy of folders. Some are "special" folders (Inbox, Sent Items, Drafts, etc.) that need specific handling during import. Detection uses a 3-tier fallback strategy:

Tier 1: Message Store Entry IDs

The PST message store (pst.get_message_store()) may contain entry ID properties that directly identify special folders:

Property Tag Folder
PR_IPM_SUBTREE_ENTRYID 0x35E0 IPM Subtree
PR_IPM_OUTBOX_ENTRYID 0x35E2 Outbox
PR_IPM_WASTEBASKET_ENTRYID 0x35E3 Deleted Items
PR_IPM_SENTMAIL_ENTRYID 0x35E4 Sent Items

Each entry ID is 24 bytes. The last 4 bytes (LE uint32) contain the folder identifier, which matches folder.get_identifier() from pypff.

PR_VALID_FOLDER_MASK (0x35DF) indicates which entry IDs are valid:

Bit Folder
0 IPM Subtree
1 Inbox
2 Outbox
3 Deleted Items
4 Sent Items
7 Finder / Search

In locally-created Outlook PSTs, all bits are typically set (0xFF) and Tier 1 detects all special folders. In Exchange/O365-exported PSTs, only some bits may be set — for example 0x89 (subtree + wastebasket + finder only), meaning Sent Items, Outbox, and Inbox entry IDs are absent and Tier 1 only detects Deleted Items. The remaining folders must be detected by Tier 2 or 3.

PST type PR_VALID_FOLDER_MASK Tier 1 detects Needs fallback for
Local Outlook 0xFF All special folders Nothing
Exchange/O365 migration 0x89 Deleted Items only Inbox, Sent, Outbox, Drafts
Other/unknown varies Whatever IDs are valid Anything missing

Tier 2: SourceWellKnownFolderType (Named Property)

PSTs exported by Microsoft's migration tools (Exchange/O365) contain a named property that identifies special folders. This covers exactly the gap left by Tier 1 on Exchange PSTs — Inbox, Sent Items, Outbox, and Drafts:

  • GUID: {9137a2fd-2fa5-4409-91aa-2c3ee697350a}
  • Name: SourceWellKnownFolderType

This is a named property, so its tag varies between PST files (resolved via the Name-to-ID Map at NID 0x61). The values are:

Value Folder Type
10 Inbox
11 Sent Items
12 Outbox
14 Deleted Items
17 Drafts

The resolution process:

  1. Read the Entry Stream, GUID Stream, and String Stream from the Name-to-ID Map
  2. Parse NAMEID records (8 bytes each) to find the one matching the target GUID + string name
  3. Calculate the NPID: 0x8000 + wPropIdx
  4. Read that property tag from each folder

This tier is only available on Exchange/O365 migration PSTs. It is absent from locally-created Outlook PSTs (which have all entry IDs via Tier 1 anyway).

Tier 3: Folder Name Matching

As a final fallback, folder names are matched against a dictionary of known Outlook default folder names in multiple languages (English, French, German, Spanish, Italian, Dutch, Portuguese, Russian, Polish, Czech, Hungarian, Danish, Norwegian, Swedish, Finnish, Turkish, Japanese, Chinese, Korean, Arabic, Hebrew, Ukrainian, Romanian).

This matching is only applied to direct children of the IPM subtree to avoid false positives on user-created subfolders with coincidental names.

Detection Priority

For each folder, detection is attempted in this order:

  1. Entry ID match (Tier 1) — highest priority
  2. SourceWellKnownFolderType match (Tier 2)
  3. Folder name match (Tier 3) — only for IPM subtree direct children
  4. Normal folder (no special handling)

IPM Subtree

The "Top of Personal Folders" wrapper folder is skipped by locating the IPM subtree via PR_IPM_SUBTREE_ENTRYID on the message store. All folder iteration starts from the IPM subtree, not the root folder. This excludes internal folders like "Freebusy Data", "Search Root", etc.

Message Processing

EML Reconstruction

Each pypff message is converted to RFC 5322 format by reconstruct_eml():

  1. Transport headers (if available): Used for threading headers (Message-ID, In-Reply-To, References), sender, recipients, date
  2. MAPI properties (fallback): Sender from PR_SENDER_* properties, recipients from the recipient table, date from delivery_time / client_submit_time
  3. Body: Plain text and/or HTML body
  4. Attachments: Filenames from PR_ATTACH_LONG_FILENAME / PR_ATTACH_FILENAME MAPI properties, with MIME type from PR_ATTACH_MIME_TAG

Sender Resolution

For Exchange (EX) address types, SMTP addresses are resolved in order:

  1. PR_SENDER_SMTP_ADDRESS
  2. PR_SENDER_EMAIL_ADDRESS (if it contains @)
  3. sender_name parsed as email
  4. Store owner email from message store PR_DISPLAY_NAME (fallback for Exchange sent items with no SMTP address)

Flag and Label Mapping

Folder Type IMAP Label IMAP Flags is_import_sender
Inbox (none) False
Sent Items Sent True
Drafts (none) \Draft False
Deleted Trash False
Outbox OUTBOX True
Normal folder path False

Subfolders of special folders inherit the parent's special type (so they keep is_import_sender, IMAP flags, etc.) and get their own subfolder name as an additional label. For example, a "Sent Items/Archives 2024" subfolder yields is_import_sender=True + labels Sent and Archives 2024. Deeper nesting builds hierarchical paths: "Sent Items/Projects/Work" yields labels Sent and Projects/Work.

Per-message flags from MAPI properties:

  • MSGFLAG_READ → \Seen
  • MSGFLAG_UNSENT or Drafts folder → \Draft
  • FLAG_STATUS >= 2 (follow-up) → \Flagged

Chronological Ordering

Messages are collected in a first pass (lightweight metadata only), sorted by delivery_time (oldest first, None timestamps last), then reconstructed to EML one at a time in the second pass. This ensures proper threading while limiting memory usage.

Failure handling

  • Unreadable archive. A corrupt/truncated PST (bad signature, or a MAPI tree missing its root folder / message store) fails the whole run with a PSTFileUnreadableError whose message carries the PST_UNREADABLE marker — the importer modal matches that marker to show a "retrying won't help, re-generate the archive" message instead of the generic error. Checked up-front (assert_pst_readable) so a bad file fails fast, not deep in the walk.
  • Oversized attachment. An attachment beyond the per-message size budget (MAX_INCOMING_EMAIL_SIZE) fails the whole message (raised as OversizedAttachmentError, counted in failure_count) rather than silently delivering a truncated copy that looks like a success.
  • Unreadable single message. A message libpff can't read, or whose EML reconstruction raises, is counted as one failure (eml_bytes=None) and the walk continues — one bad message never aborts the import.

Key Files

File Description
core/services/importer/pst.py PST parsing, folder detection, EML reconstruction; run_pst runner: flag/label logic, progress reporting, resume
core/services/importer/tasks.py run_import_task orchestration: guards, run lock, watermark hand-off, terminal transition
core/services/s3_seekable.py S3-backed seekable file reader with LRU cache
core/tests/importer/test_pst_import.py Unit and integration tests

Dependencies

  • pypff (libpff-python): Python bindings for reading PST files. Requires build-essential for compilation in Docker.