# PST Archive — Mail Indexer A self-hosted, Dockerized application for **uploading Microsoft Outlook PST/OST files**, **indexing every email inside them**, and **searching the contents through a web browser** — with full **user authentication**, **multi-factor authentication (MFA)**, **encrypted-at-rest credentials**, and a **built-in administration section** for admin users. Designed for **forensic review, e-discovery, personal archive browsing, and migration audits** — any situation where you need to crack open a PST and actually find things inside it without loading it into Outlook. --- ## What's new in v2.2 - **Administration now runs inside the main application** — there is no longer a separate admin process or admin port (previously the panel ran on its own port, e.g. mapped to `9001`). Everything is served on the single app port (`8000` by default). - **"Administration" button** — admin users see an **ADMINISTRATION** button in the top-right of the masthead, next to their username. It opens the administration section at `/admin`. The button is not shown to non-admin users. - **Access is restricted to admins** — the administration UI and every `/api/admin/*` endpoint require an authenticated session **and** the `admin` role. Non-admins receive `403`; unauthenticated requests receive `401`. - **Footer** — every in-app page (the archive view and the administration view) now shows `© 2026 Martinhal IT - Joao Vaz - Version 2.2`. The login/setup screen has no footer. - **Updating is drop-in** — extract this release over your existing install and rebuild; see [Updating to a new version](#updating-to-a-new-version). Your `./data` directory (database, uploaded PSTs, encryption key) is never touched. > Historical note: some deep-dive sections below still describe the original > two-port design (main app + separate admin panel) for reference. Operationally, > as of v2.2 both are the same process on one port, with administration at > `/admin`. --- ## Table of Contents 1. [Features](#features) 2. [Quick Start](#quick-start) 3. [First-Run Setup](#first-run-setup) 4. [Running as a Daemon](#running-as-a-daemon) 5. [Theming & Dark Mode](#theming--dark-mode) 6. [Authentication & Security Model](#authentication--security-model) 7. [Admin Panel](#admin-panel) 8. [Organizing PSTs with Folders](#organizing-psts-with-folders) 9. [Folder Access Permissions](#folder-access-permissions) 10. [Multi-Factor Authentication (MFA)](#multi-factor-authentication-mfa) 11. [Encryption at Rest](#encryption-at-rest) 12. [How It Works](#how-it-works) 13. [Architecture](#architecture) 14. [Project Layout](#project-layout) 15. [Data Model](#data-model) 16. [REST API Reference](#rest-api-reference) 17. [Frontend Interfaces](#frontend-interfaces) 18. [Search Syntax](#search-syntax) 19. [Configuration](#configuration) 20. [Persistence & Backup](#persistence--backup) 21. [Troubleshooting](#troubleshooting) 22. [Performance Notes](#performance-notes) 23. [Security Considerations](#security-considerations) 24. [Extending the App](#extending-the-app) --- ## Features ### Core - **Drag-and-drop upload** of `.pst` and `.ost` files through the browser - **Streaming upload with progress bar** — handles multi-gigabyte archives - **Background indexing** into SQLite with **FTS5 full-text search** (ranked, prefix-matched, with highlighted snippets) - **Multi-archive support**: switch between uploaded PSTs; each is searched independently - **Folder organization**: create folders in the sidebar to group related PST files (e.g. by client, case, year). Move archives between folders, rename folders, or delete folders (any archives inside are safely moved to Uncategorized) - **Per-folder access permissions**: admins can restrict which non-admin users can view each folder. A single user can be granted access to many folders; a single folder can be shared with many users (true many-to-many) - **Folder filter**: narrow search to any folder within the archive (Inbox, Sent Items, nested folders) - **Full message viewer**: subject, sender, recipients, date, folder, attachments, body - **Persistent storage**: everything lives in a mounted `./data` volume ### Security - **First-run setup wizard** forces creation of an initial administrator before any other action is possible - **Password authentication** using **Argon2id** (winner of the Password Hashing Competition) with constant-time verification - **TOTP-based MFA** (RFC 6238; compatible with Google Authenticator, Authy, 1Password, Microsoft Authenticator, and every other standard authenticator app) - **Encryption at rest** for all user PII — usernames, password hashes, and TOTP secrets are stored as Fernet (AES-128-CBC + HMAC-SHA256) ciphertexts - **Master key** auto-generated on first run and stored with `chmod 600`, or overridable via `MASTER_SECRET` environment variable - **HMAC-based username lookup** — usernames are never stored in plaintext but are still indexable for login - **JWT session cookies**, signed with an HKDF-derived sub-key, HttpOnly, SameSite=Lax - **Rate limiting** on login attempts (10 failures per 5 minutes per IP+username) - **Admin-only user management**: create, delete, role-change, password-reset, MFA-enroll other users - **Admin file & folder management**: system-wide view of every uploaded PST and every folder, with create/rename/delete for folders and force-delete for any file regardless of uploader - **Built-in administration section** for admins (at `/admin`) — restricted to the `admin` role, and lockable at the path level behind a reverse proxy/VPN ### UI - **Light and dark themes** on both the main app and the admin panel, with a sun/moon toggle in the masthead and on login/setup screens - Preference persists in `localStorage` per-browser and respects the operating system's `prefers-color-scheme` on first visit - Theme applied inline in `
` before stylesheets load — **no flash of wrong theme** on page load - Dark mode uses a custom "inked paper" palette (deep charcoal with warm paper-cream text and softened oxblood accent) — intentional character, not generic-dark - Editorial typography (Fraunces serif + JetBrains Mono), grain overlay, responsive layout down to mobile widths --- ## Quick Start ### Prerequisites - Docker 20.10 or newer - Docker Compose v2 - A `.pst` or `.ost` file to index ### Run (as a daemon — recommended) ```bash docker compose up -d --build ``` The `-d` flag runs the container in the background (detached). On the first run this takes **3–5 minutes** because `libpff-python` compiles a C extension from source; subsequent startups are nearly instant. The container is configured with `restart: unless-stopped`, so it will automatically come back up after a reboot or a crash, and keep running across Docker daemon restarts — until you explicitly stop it. Once the container is up: - **App**: `http://localhost:8000` - **Administration**: `http://localhost:8000/admin` — or just click the **ADMINISTRATION** button in the top-right after signing in as an admin. (Non-admin users do not see the button and cannot reach `/admin`.) ### Foreground mode (useful for debugging) ```bash docker compose up --build ``` This runs the container attached to your terminal so you can see logs inline. Press `Ctrl-C` to stop. ### Tail the logs ```bash docker compose logs -f # follow both streams docker compose logs -f pst-indexer # explicit service name ``` ### Check status ```bash docker compose ps # is it running? docker compose top # which processes? ``` ### Stop ```bash docker compose stop # stop, keep data, keep container docker compose down # stop and remove the container, keep data volume ``` ### Full reset ```bash docker compose down rm -rf ./data # erases everything including users and the master key docker compose up -d --build ``` ### Restart / update ```bash # After editing code or pulling new images: docker compose up -d --build # rebuild & restart with one command # Or just restart without rebuilding: docker compose restart ``` --- ## First-Run Setup On the very first visit to `http://localhost:8000`, you will be presented with a **setup wizard** that asks you to create the initial administrator: 1. Choose a username (3–64 characters) 2. Choose a password (8+ characters — use a passphrase you'll remember) 3. Confirm the password The setup endpoint is only callable **while the users table is empty**. Once the first admin is created, the endpoint returns `409 Conflict` and is effectively disabled. After setup, you are automatically signed in and landed on the main app. **Heads up:** the very first account created through the setup wizard is an **administrator**. After that, sign in normally and use the **ADMINISTRATION** button (top-right) to manage users, folders, files, and permissions. The administration section and its API are only accessible to admin users. --- ## Running as a Daemon By default, this app is designed to **run as a long-lived background service**. The `docker-compose.yml` sets `restart: unless-stopped`, which means once started, the container will: - Keep running after you close your terminal - Automatically restart if the process crashes - Automatically restart when the host reboots and Docker comes back up - **Not** restart if you explicitly stop it with `docker compose stop` (hence "unless-stopped") ### Starting the daemon ```bash docker compose up -d --build ``` The `-d` flag is short for "detached" — the container is launched in the background and the command returns immediately. Your terminal is free to do other things; the container keeps running. ### Verifying it's running ```bash docker compose ps ``` You should see output like: ``` NAME IMAGE STATUS PORTS pst-indexer pst-indexer-pst-indexer Up 2 minutes (healthy) 0.0.0.0:8000->8000/tcp ``` The `(healthy)` flag comes from the built-in healthcheck, which probes both `/api/health` endpoints every 30 seconds. ### Watching logs Since the container is detached, you won't see logs in your terminal by default. Attach to them on demand: ```bash docker compose logs # print recent logs and exit docker compose logs -f # follow (live tail), Ctrl-C to detach docker compose logs -f --tail=100 # follow, starting from the last 100 lines docker compose logs --since 1h # only logs from the last hour ``` Detaching from `-f` does **not** stop the container — it just stops showing you the logs. ### Stopping ```bash docker compose stop # stop the container but keep it around docker compose start # start it again if previously stopped docker compose restart # stop and start in one command docker compose down # stop and remove the container (data volume is kept) docker compose down -v # stop, remove container, AND remove anonymous volumes ``` None of these delete your `./data` directory — that's a bind mount, not a Docker-managed volume, so it's always yours. ### Survives reboots Because `restart: unless-stopped` is set, after a host reboot Docker will automatically start the container as soon as the Docker daemon comes up. Nothing manual required on your part. If you ever explicitly stopped it with `docker compose stop`, Docker will *not* auto-start it after reboot (that's the whole point of "unless-stopped"). Run `docker compose start` or `docker compose up -d` to bring it back. ### Updating to a new version This release is a **drop-in update**: extract the archive over your existing installation directory (the one containing `docker-compose.yml` and your `data/` folder) and rebuild. The archive contains only application code — it has **no `data/` directory**, so your database, uploaded PSTs, and encryption key are never overwritten. ```bash # From the parent directory that holds your existing install folder: # (the folder with docker-compose.yml and data/ inside it) 7z x pst-archive-v2.2.7z # extract, overwriting code files in place # Then rebuild and recreate the container: cd