mirror of
https://github.com/suitenumerique/messages.git
synced 2026-08-30 03:39:41 +02:00
214 lines
8.4 KiB
Markdown
214 lines
8.4 KiB
Markdown
# 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:** `process_pst_file_task` in `core/services/importer/pst_tasks.py`
|
|
|
|
## 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_tasks.py ── Celery task, flag/label mapping, progress reporting
|
|
│
|
|
▼
|
|
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/importer/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.
|
|
|
|
## Key Files
|
|
|
|
| File | Description |
|
|
|------|-------------|
|
|
| `core/services/importer/pst.py` | PST parsing, folder detection, EML reconstruction |
|
|
| `core/services/importer/pst_tasks.py` | Celery task, flag/label logic, progress reporting |
|
|
| `core/services/importer/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.
|