# AGENTS.md — working instructions for InLook > **Read this first.** This is the standing context for InLook so you (or any AI > tool / new machine) can resume work without re-learning the basics. Keep it up > to date: when the architecture, commands, conventions, or roadmap change, > update this file in the same change. ## 1. What this project is **InLook** — a small, fast, *safe* viewer for `.eml` (RFC 822) and Outlook `.msg`/`.oft` (CFB/MAPI) email files. Free Software from **Struis ICT**. It opens one email file and renders headers + body + attachment list in a native window. It is intentionally a *viewer*, not a mail client: no accounts, no network, no sending. - **Language:** Rust (edition 2021, MSRV **1.88**, developed on 1.95). - **Crate name / binary:** `inlook`. - **Canonical repo:** (`origin`). - **License:** `MIT OR Apache-2.0`. - **Current version:** see `.release-please-manifest.json` (source of truth). ## 2. Architecture (~1100 lines) | File | Responsibility | |---|---| | `src/lib.rs` | Library crate root. Exposes the pure, GUI-free core (`pub mod render; pub mod msg`) so it can be unit-, snapshot- and fuzz-tested without a window. Keep it free of I/O and platform glue. | | `src/main.rs` | Binary (thin shell over the lib). CLI arg parsing (`--version`, `--help`, `register`, `unregister`, ``, or no-arg → **welcome screen**). Builds the `tao` window + `wry` WebView. The window can load files in place: browse (`inlook://browse`), drag-and-drop, and About-panel external links are routed through `EventLoopProxy` so the loop swaps the shared `doc`/`current_bytes` and reloads. Windows console-attach shim for CLI output. | | `src/render.rs` | `render_file_to_html(bytes, path)` — the single entry point — dispatches on the CFB magic to the `.msg` or `.eml` path; both feed one shared `page()` builder, so **every format gets identical escaping/sandbox/CSP treatment**. Has the unit tests. | | `src/msg.rs` | Pure Outlook `.msg`/`.oft` parsing on the `cfb` crate: MAPI property streams (subject, sender, display-to/cc, FILETIME date, text/HTML bodies) and attachment names+sizes (payloads never read). Capped reads, lossy decoding, hostile input degrades to `None`. RTF-only bodies are not decoded (headers + attachments still render). | | `src/version.rs` | Pure semver comparison (`is_newer`) for the update check. | | `src/update.rs` | Windows-only, binary-only. Update check via WinHTTP (OS TLS) reading GitHub's latest-release redirect `Location` header. `maybe_run` = **opt-in** auto-check on startup (consent in HKCU, once per new version). `check_now` = **on-demand** check from About -> "Check for updates" (always reports a result; the click is its own consent). Never downloads/runs anything; no auto network without consent. | | `src/registry.rs` | Windows-only, binary-only. `register()`/`unregister()` write the `.eml` file association into `HKLM\Software\Classes` (ProgID `StruisICT.InLook`) **plus Default Programs registration** (`RegisteredApplications` + `Capabilities`), notify the shell, then deep-link into Settings (`ms-settings:defaultapps?registeredAppMachine=InLook`) so the user finishes with one click — Windows never lets an app set the UserChoice default itself. Requires elevation. | | `tests/snapshots.rs` | Golden-file snapshot tests: renders every `tests/fixtures/*.{eml,msg}` and compares against `tests/snapshots/.html`. Regenerate with `INLOOK_UPDATE_SNAPSHOTS=1 cargo test --test snapshots`, then review the diff. `.msg` fixtures are generated by `cargo run --example gen_msg_fixtures`. | | `tests/large_emails.rs` | Realistic large-message cases (Outlook-style): ~20 MB `.eml`/`.msg` with sizeable attachments — asserts they parse, list attachments with correct sizes, extract exact bytes, and are **not** inlined into the page. Also the oversized-`cid:`-image budget. | | `tests/properties.rs` | Property tests (`proptest`) over `render_file_to_html`: no input — arbitrary bytes, CFB-shaped bytes, or arbitrary subject/body — may panic or emit a raw `` via `srcdoc`, *and* the inner document carries a strict CSP (`default-src 'none'; img-src data:; ...`). Two independent layers. - The outer page's strict CSP is sent both as an HTTP header (from the custom protocol handler in `main.rs`) and as a `` tag. No remote anything; inline `data:` images only. No scripts ever run from email content. - `#![deny(unsafe_code)]` is on. The only `unsafe` is explicitly `#[allow(unsafe_code)]`-annotated Win32 calls (console attach, shell notify) with a `// Reason:` comment. Keep that pattern for any new FFI. ## 3. Everyday commands ```sh cargo build --release # build the binary cargo run --release -- test/sample.eml # run against the sample email cargo test --release # unit tests (render.rs) + snapshot tests (tests/) INLOOK_UPDATE_SNAPSHOTS=1 cargo test --test snapshots # regenerate golden snapshots cargo +nightly fuzz run render_eml -- -max_total_time=60 # fuzz (Linux/macOS, needs cargo-fuzz) cargo fmt --all # format cargo fmt --all -- --check # CI format gate cargo clippy --all-targets --release -- -D warnings # CI lint gate (warnings = errors) ``` **Before pushing, run the same gates CI runs:** `fmt --check`, `clippy -D warnings`, `test --release`, `build --release`. CI (`.github/workflows/checks.yml`) runs them on Windows, Linux, and macOS, plus a `cargo audit` security check. Linux dev/CI system deps (WebKitGTK stack for `wry`): ```sh sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev \ libayatana-appindicator3-dev librsvg2-dev ``` ## 4. Conventions - **Commits:** Conventional Commits (`feat:`, `fix:`, `chore:`, `ci:`, `docs:`, …, with optional scope like `feat(packaging):`). This drives release-please versioning and the changelog — so commit messages matter. See [§5.1 Versioning (SemVer)](#51-versioning-semver) for the commit→version map. - **Branches:** topic branches like `feat/...`, `fix/...`, `chore/...`; PR into `main`. `main` is the release line. - **Style:** rustfmt defaults; clippy must be clean with `-D warnings`. - **Tests:** unit tests live next to the code (`#[cfg(test)] mod tests` in `render.rs`); rendered-output changes are covered by the golden snapshots in `tests/`. Any new body/header/attachment handling should add a test, especially escaping/sandboxing assertions, and new render behaviour should add a fixture (`tests/fixtures/*.eml`) + regenerated snapshot. - **No scope creep:** InLook is a viewer. Don't add sending or account features. Suggest improvements, but keep the safe-by-default posture. The one sanctioned network path is the **opt-in, off-by-default** update check (`src/update.rs`, Windows only): no call happens without explicit consent, it uses the OS HTTPS stack (WinHTTP — no bundled HTTP/TLS crate), and it only reads the latest release tag to notify — never downloads or runs anything. Keep it opt-in and keep the README privacy policy accurate if you touch it. ## 5. Releasing (automated) ### 5.1 Versioning (SemVer) InLook versions follow **[Semantic Versioning 2.0.0](https://semver.org/)**: `MAJOR.MINOR.PATCH`. Because InLook is an *application*, its "public API" — the contract we promise not to break without a MAJOR bump — is its **user-facing behaviour**: - the **CLI surface**: subcommands and flags (``, no-arg picker, `register`, `unregister`, `--version`/`-V`, `--help`/`-h`) and their exit codes; - the **file association** identifiers Windows + uninstallers rely on: ProgID `StruisICT.InLook`, the `.eml`→ProgID mapping, and the WiX MSI `UpgradeCode` (which **must never change** — see §6); - the **package identifiers** downstream channels depend on (e.g. winget `StruisICT.InLook`, Flatpak `com.struisict.InLook`). **Bump rules:** | Change | Bump | Conventional Commit | |---|---|---| | Backwards-**incompatible** change to the contract above (rename/remove a flag, change an exit code, change the ProgID/UpgradeCode/package id, drop a supported input) | **MAJOR** | `feat!:` / `fix!:` or a `BREAKING CHANGE:` footer | | Backwards-**compatible** new capability (new flag, attachment saving, new platform/package) | **MINOR** | `feat:` | | Backwards-compatible bug fix or internal change with no behaviour change (render fixes, dependency bumps) | **PATCH** | `fix:` | | No release on its own | — | `chore:`, `docs:`, `ci:`, `refactor:`, `test:`, `build:`, `style:` | **Pre-1.0 clause (we are here, at 0.x).** Per SemVer §4, `0.y.z` is *initial development* — the contract may still change. release-please is configured with `bump-minor-pre-major: true`, so while in 0.x a **breaking change bumps the MINOR** (e.g. `0.5.0 → 0.6.0`) instead of jumping to `1.0.0`. Features stay MINOR, fixes stay PATCH. **Cut `1.0.0` deliberately** — only once the CLI and file-association contract above is considered stable. **Pre-releases** use SemVer identifiers and have *lower* precedence than the finished version: `1.0.0-alpha.1` < `1.0.0-beta.1` < `1.0.0-rc.1` < `1.0.0`. Do **not** use SemVer build metadata (`+...`); release-please/Cargo tags are plain `vMAJOR.MINOR.PATCH`. **Version source of truth:** `.release-please-manifest.json` and the `version` field in `Cargo.toml` (both bumped by the release PR). Git tags are `vX.Y.Z`. Never hand-edit either — let release-please do it. ### 5.2 Release flow Release is driven by **release-please** + GitHub Actions — do **not** hand-edit versions or `CHANGELOG.md`. 1. Land Conventional-Commit PRs on `main`. 2. `release-please` keeps an open "release PR" (`chore: release X.Y.Z`) with the bumped version (`Cargo.toml`, manifest) and generated changelog. 3. **Merging that release PR** tags the version and creates the GitHub Release. 4. `.github/workflows/release.yml` then builds and attaches per-platform artifacts: - **Windows:** `cargo build --release` → `cargo wix` (MSI) + raw `inlook.exe`. - **Linux:** `cargo deb` (.deb) + `scripts/build-appimage.sh` (AppImage). - **macOS:** build `aarch64` + `x86_64`, `lipo` into a universal binary, `scripts/build-dmg.sh` → `.dmg`. 5. `.github/workflows/packagers.yml` updates downstream package manifests on release. (Version source of truth is covered in §5.1 above.) ## 6. Packaging files (source of truth lives in this repo) | Path | What | |---|---| | `.cargo/config.toml` | Pins `+crt-static` (MSVC) so the EXE statically links the VC++ runtime and launches on a clean Windows install. **Keep it** — removing it reintroduces the `0xC0000135` crash. | | `build.rs` + `winresource` build-dep | Windows-only: embeds `assets/inlook.ico` + version metadata (ProductName/Company/FileVersion) into `inlook.exe`. | | `wix/main.wxs` | Windows MSI manifest. **`UpgradeCode` must never change** (keeps upgrades in-place). Version comes from `$(var.Version)`. Icon comes from `assets/inlook.ico` (never the EXE). | | `packaging/signpath/README.md` | **SignPath Foundation code signing** (the active signing route): free OSS Authenticode cert, wired into `release.yml` behind the `SIGNPATH_ORGANIZATION_ID` var + `SIGNPATH_API_TOKEN` secret; every release needs a manual approval in the SignPath portal. The README's "Code signing policy" section is a Foundation requirement — keep it accurate. | | `scripts/sign-windows.ps1` | Legacy PFX signing fallback. No-op unless secrets `WINDOWS_CERT_PFX_BASE64` + `WINDOWS_CERT_PASSWORD` are set (newly issued certs no longer come as PFX — SignPath above is the real route). | | `Cargo.toml` `[package.metadata.deb]` | Debian `.deb` config (incl. hicolor icon assets). | | `scripts/build-appimage.sh`, `scripts/build-dmg.sh`, `scripts/generate-icons.py` | Linux AppImage / macOS dmg builders; icon-set generator. `build-dmg.sh` code-signs with the hardened runtime when `APPLE_SIGNING_IDENTITY` is set. | | `packaging/macos/` | Apple Developer ID signing + notarization: wired into `release.yml` behind the `APPLE_*` secrets/variable, gated like SignPath (unsigned until configured). See its `README.md` for the one-time setup. `assets/macos/entitlements.plist` holds the hardened-runtime entitlements (JIT for WKWebView only). | | `assets/` | `inlook.ico` (Windows), `inlook.png` + `icons/inlook-*.png` (Linux hicolor), `inlook.desktop`, `Info.plist` (macOS). | | `packaging/winget/` | winget submission notes + validated reference manifest (`StruisICT.InLook`). See its `README.md` and the PR #379422 post-mortem. | | `packaging/flatpak/` | Flathub submission (`com.struisict.InLook.*`). See its own `README.md`; regenerate `generated-sources.json` whenever `Cargo.lock` changes. | | `packaging/homebrew/inlook.rb` | Homebrew cask source of truth. | ## 7. Dependencies & upgrade policy Core: `mail-parser` (MIME parsing), `tao` (window), `wry` (WebView), `rfd` (file/message dialogs), `html-escape`. Windows-only: `windows`, `windows-registry`. Dependabot (`.github/dependabot.yml`) opens **patch/minor** bumps weekly (grouped). **Major** bumps of `wry` and `windows-registry` are *ignored* by Dependabot on purpose — their builder/error APIs changed in 0.5x/0.6x and need a single coordinated manual upgrade PR with code adaptation + a manual window/render smoke test. ## 8. Current state (update this section as work lands) - **Version:** approaching **1.0.0** (last release 0.9.0). Features shipped since 0.5.0: `.msg`/`.oft` support, attachment save + nested-message open, inline `cid:` images, opt-in + on-demand update check, welcome screen with drag-drop + About menu, window icon, per-process WebView2 data folder. - **Deps:** `tao` is on **0.35** (the multi-major jump built cleanly with wry 0.45 — they're decoupled via `raw-window-handle`; verified GUI at runtime). `wry` stays at 0.45 (bumping to 0.55 is a separate, larger API migration — dependabot ignores wry majors on purpose; see §7). GitHub Actions and `html-escape` kept current. ## 9. Roadmap / ideas (not yet built) Prioritised, viewer-appropriate features: 1. **Power-user / technical view** — an opt-in panel for people who want the plumbing, not just the rendered message. Off by default so the normal view stays clean; toggled from the app bar (and pure-CSS, no scripts, like the About overlay). Should surface: - **All headers** verbatim, plus the **raw RFC 822 source** ("View source"). - **Routing** — the `Received:` hop chain, parsed and in delivery order. - **Authentication results** — SPF / DKIM / DMARC pass/fail from `Authentication-Results` / `Received-SPF` (display only, no revalidation). - **MIME structure** — the part tree with content-types, encodings, sizes; for `.msg`, the parsed MAPI properties / named streams. - **Metadata** — message size, dates (Date vs Received), Message-ID. Keep everything HTML-escaped and offline; this is inspection, not action. 2. **Plain-text ↔ HTML toggle** when both parts exist. (Shipped, formerly on this list: save/open attachments, inline `cid:` images, drag-and-drop + multi-file open — see section 8 / CHANGELOG.) When you pick one up, add a test, follow the commit convention, and update sections 8–9 here.