# mailx Host Abstraction + msgview Parity Plan

**Status:** step 1 (abstraction package) implemented 2026-04-20. msgview adapter (step 2+) deferred until a Mac client or arm64-without-webkit fallback is actually needed. Resume cold from this doc.

## Why this exists

1. **msger covers most of what we need.** Windows + mainstream x86_64 Linux run msger fine today. The Rust/wry binary links against `libwebkit2gtk-4.1.so.0`; on Debian trixie / Raspberry Pi OS arm64 you install it once (`sudo apt install libwebkit2gtk-4.1-0`) and msger works. Not a regression, just a missing system dep. Older distros with only webkit2gtk 4.0 would need an msger rebuild.
2. **msgview fills specific gaps.** Electron's bundled Chromium is the natural path for Mac (where msger would need separate webkit work) and a fallback for niche Linux systems where webkit2gtk-4.1 isn't available and can't be installed. It also sidesteps two msger pain points — multi-monitor drag bugs and WebView2 Evergreen auto-update on Windows — but those aren't load-bearing reasons to move existing Windows/Linux users off msger.
3. **We want mailx to run on both without the call sites caring.** mailx imports a host abstraction; the abstraction picks msger or msgview at runtime. Step 1 (done 2026-04-20) puts the seam in place even before msgview lands, so future platform work is an adapter drop-in instead of a call-site rewrite.
4. **msger and msgview must stay app-agnostic.** Architectural rule from mailx/CLAUDE.md: *"msger MUST have ZERO knowledge of mailx, iflow, or any application code."* Same rule extends to msgview. Any mailx-specific glue living in the wrappers today must move to mailx. The 2026-04-20 `_msgapi*` service-callback rename in msger Rust was the prerequisite that made both hosts share one generic JS contract.

## Current coupling (survey)

Searched msger + msgview for `mailx` references:

- **msger/shower.ts:125** — doc comment on `setAppName()` says `e.g. setAppName("mailx")`. That's a doc reference only; `setAppName` itself is generic (app passes its own name for per-user bin dir + AUMID). OK to leave, or reword the example to `"myapp"`.
- **msger/msger-plan.md:70** — explicit rule `"mailxapi.js content (belongs in mailx)"`. Already correct.
- **msger Rust src** — no mailx/iflow/oauth references. Clean.
- **msgview** — no mailx references. Pristine.

**Conclusion:** the wrappers are already app-agnostic in code. The factoring work is on the mailx side: today mailx calls `showMessageBox`/`showService` directly from `@bobfrankston/msger`. We introduce a host abstraction so the import swap is one line, and document the contract so the next app (msga, etc.) can use the same interface.

## Current mailx → msger surface

From `bin/mailx.ts:825-900` and `client/lib/mailxapi.js`:

```ts
import { showMessageBox, showService, setAppName } from "@bobfrankston/msger";

// Desktop launch:
const handle = showService({
    title, url, contentDir, initScript, icon, aumid,
    size: { width, height }, pos: { x, y },
    escapeCloses: false,
});

handle.onRequest(async (req) => { /* dispatch req._action → MailxService */ });
handle.send({ _cbid, result });   // responses and push events
// handle.close(); handle.result (promise resolving on window close)
```

**In-WebView side** (`client/lib/mailxapi.js` injected via `initScript`):
- Exposes `window.callNode(action, args) → Promise<result>` built on `window.msgapi.sendToHost(...)`
- Receives server-push events via the IPC channel
- `api-client.ts` auto-detects IPC vs HTTP and routes through `callNode` when IPC is present

**That's the entire surface.** Small and stable.

## The `MailxHost` interface

One interface, two implementations. Lives in mailx (not in msger/msgview).

```ts
// packages/mailx-host/index.ts
export interface MailxHostOptions {
    title: string;
    url: string;                 // relative to contentDir (e.g. "index.html")
    contentDir: string;          // served via custom protocol
    initScript: string;          // mailxapi.js contents, injected pre-page
    icon?: string;
    aumid?: string;              // Windows taskbar identity
    size?: { width: number; height: number };
    pos?: { x: number; y: number };
    escapeCloses?: boolean;
}

export interface MailxHostHandle {
    onRequest(fn: (req: any) => void | Promise<void>): void;
    send(msg: any): void;        // push to WebView
    close(): void;
    result: Promise<{ closed: boolean }>;
}

export interface MailxHost {
    setAppName(name: string): void;
    showService(opts: MailxHostOptions): MailxHostHandle;
}
```

Two impls:

- **`packages/mailx-host-msger/index.ts`** — wraps `@bobfrankston/msger`, delegates 1:1. ~30 LOC.
- **`packages/mailx-host-msgview/index.ts`** — wraps Electron. Creates `BrowserWindow`, installs `protocol.handle()` for `contentDir`, sets `webPreferences.preload` pointing at a shim that loads the `initScript` string and exposes the same `window.msgapi` / `window.callNode` surface, wires `ipcMain.handle` ↔ `handle.onRequest` / `webContents.send` ↔ `handle.send`. ~150 LOC including the preload shim.

Selection at runtime in `bin/mailx.ts`: env var `MAILX_HOST=msger|msgview` (default msger on Windows, msgview on Linux/Mac), or `--host=` flag.

## msgview parity checklist (from msgx/parity.md)

Mapping each msger capability to Electron:

| msger feature | Electron equivalent | Work needed |
|---|---|---|
| Service mode (bidir IPC via stdin/stdout) | `ipcMain.handle` / `webContents.send` | Wire in host shim. Electron's IPC is in-process, strictly better than JSON-line stdio. |
| `initScript` injection | `webPreferences.preload` | Preload shim evaluates the injected script string in page context before load. |
| `msger.localhost` custom protocol | `protocol.handle()` (Electron 25+) | Register in host shim on `app.whenReady()`. |
| `window.msgapi` (TCP/UDP/HTTP, UI control) | Expose via preload + `contextBridge` | Re-implement the subset mailx uses — TCP for iflow transport, UI control (resize, minimize, etc.). Node APIs available in main process, expose narrow surface. |
| AUMID for taskbar pin | `app.setAppUserModelId(aumid)` | One line. |
| Navigation gating (same-origin stays, cross-origin external) | `webContents.setWindowOpenHandler` + `will-navigate` | Reuse msger's `url_origin()` logic. |
| Multi-monitor drag | Chromium native | Disappears for free — msger's `ScaleFactorChanged` bug doesn't exist here. |
| WebView2 Evergreen auto-update killing windows | N/A | Chromium bundled, doesn't apply. |
| Stable exe path / timestamped-copy dance | N/A | Electron installer handles this differently (Squirrel/NSIS); no hardlink trick needed. |
| EPERM cleanup on npm upgrade | N/A | Different install mechanism. |
| **Detach mode** (child outlives caller) | Harder | See below. |
| Log path option | Electron `app.setPath("logs", ...)` | Trivial. |

### Detach mode — the one real design decision

msger's detach mode lets the mailx launcher exit while the window keeps running in a forked msger process. Electron doesn't have a natural equivalent — Electron apps are one process tree rooted at the Electron main process.

**Option A: launch Electron as a separate binary.** Install `msgview` as a CLI (`msgview --url=… --content-dir=… --init-script=…`), mailx's host-msgview shim spawns it detached. Closest analog to msger's current model. Downside: two processes (mailx service + Electron), and the service must manage IPC over stdio again.

**Option B: run mailx service INSIDE the Electron main process.** mailx becomes an Electron app. `bin/mailx.ts` bootstraps Electron, creates `MailxService` + `ImapManager` in the main process, opens `BrowserWindow` with preload. Single process, IPC is in-process calls. No detach needed — the window IS the service. This is cleaner and matches how msgview is already structured for its own use case.

**Recommendation: Option B.** Requires `bin/mailx.ts` to detect host mode and, when msgview is selected, re-exec under Electron (or use Electron as the node runtime from the start). The `mailx` CLI entry still works — it just launches Electron instead of plain node. For `--server` mode we keep plain node (no window needed, so no Electron).

## Factoring plan — keep msger/msgview independent of mailx

Where mailx-specific code lives today and where it needs to end up:

| Code | Today | Target |
|---|---|---|
| `client/lib/mailxapi.js` | mailx | mailx (unchanged — already correct) |
| `packages/mailx-service/jsonrpc.ts` | mailx | mailx (unchanged) |
| `bin/mailx.ts` direct `import "@bobfrankston/msger"` | mailx | Replace with `import { getHost } from "@mailx/host"` |
| `showService` options shape (title, url, initScript, aumid, etc.) | msger-specific type | Move to `packages/mailx-host/` — msger stays generic, mailx owns the mailx-flavored option shape |
| `setAppName("mailx")` | called from mailx | Still called from mailx, but through the host abstraction |
| `window.msgapi` surface | msger defines it | Keep as msger's native API. `mailx-host-msgview` re-implements the same shape via Electron preload so mailx code in the WebView doesn't care which host it's under. |
| Per-user bin dir `%LOCALAPPDATA%\mailx\bin\` | msger's `getUserBinDir(_appName)` | Unchanged — `setAppName("mailx")` already drives this generically |

**Net effect:** msger and msgview keep zero mailx references. mailx gains three small packages (`mailx-host`, `mailx-host-msger`, `mailx-host-msgview`) that encapsulate the coupling. Next app (msga, etc.) can either copy the `mailx-host-*` pattern or use msger/msgview directly.

### The `window.msgapi` question

mailx code running in the WebView currently calls `window.msgapi.sendToHost(...)`. That's a msger-defined global. Two paths:

- **Keep the name.** `mailx-host-msgview`'s preload exposes `window.msgapi` with the same shape. mailx WebView code is unchanged. msger owns the de-facto standard; msgview implements it.
- **Rename to `window.mailxHost`.** mailx owns the name; msger and msgview each expose it via their respective injection mechanisms. Cleaner separation but more churn.

**Recommendation: keep `window.msgapi`** — it's already documented as a generic host API, msger defines the canonical shape, and msgview implementing it keeps the WebView code host-agnostic without rename churn.

## Migration sequence

1. **Create `packages/mailx-host`** with the interface + msger impl. Swap `bin/mailx.ts` to use it. Behavior identical; this is pure refactor. **Done 2026-04-20.** The package re-exports `showMessageBox` / `showService` / `setAppName` from msger and exposes a `selectHost()` seam for future impl dispatch. `MAILX_HOST=msger|msgview` env var honored; default is msger on Windows + Linux, would-be msgview on Mac (throws today until step 2 lands).
2. **Create `packages/mailx-host-msgview`** (deferred until Mac or arm64-fallback need). Add Electron as a dependency. Implement the shim. Add `msgview` package to `y:/dev/utils/msgx/msgview/` if it doesn't already expose a programmatic API (needs check — msgview currently is a standalone JSON viewer, may need its guts extracted into a library).
3. **Detach decision** (deferred). Pick Option B. Add Electron bootstrap path to `bin/mailx.ts`: when `MAILX_HOST=msgview`, re-exec under Electron. Test on Linux (pi5a) and Windows.
4. **Port `window.msgapi` surface** used by mailx into the msgview preload. Audit `client/lib/mailxapi.js` for every `msgapi.*` call — that's the minimum surface to re-implement.
5. **Iflow transport under Electron main.** `NodeTcpTransport` already uses `node:net` / `node:tls`; Electron main has full node, so this should be drop-in. Verify TLS works (Electron bundles OpenSSL, not system).
6. **Build + packaging.** Decide: separate npm packages for `mailx-host-msger` and `mailx-host-msgview`, or one package with lazy-loaded impls? Probably lazy-load so Electron isn't dragged in for Windows users. Host selection picks impl; unused impl is never required.
7. **Documentation** in msgx/parity.md: update with msgview side. (Step 1 doc updates done 2026-04-20.)

## Open questions

- **Is msgview already a library, or is it a standalone app?** Need to check `y:/dev/utils/msgx/msgview/index.ts` — if it's CLI-only, we either (a) extract its core into a library `@bobfrankston/msgview-core` or (b) just build the Electron shell directly inside `mailx-host-msgview`. (b) is simpler if msgview's current feature set doesn't include anything we need to reuse.
- **Windows: keep msger-only.** msger on Windows works well. msgview on Windows stays available via explicit `MAILX_HOST=msgview` for users who want to trade WebView2 Evergreen risk for Electron's bundle size.
- **Auto-update.** msger has timestamped-exe + stable-hardlink. Electron has Squirrel auto-update. Different mechanisms — do we care about parity here or run two update strategies?
- **AUMID across hosts.** `com.frankston.mailx` should be the same on both so taskbar pins work whichever host is running.
- **pi5a as a build host.** User mentioned moving compile tooling from pi4c to pi5a. That's a separate decision, but relevant context: Rust cross-compile for msger aarch64 is still the primary Linux arm64 path (apt-install the webkit2gtk-4.1 system dep is cheap). msgview arm64 only matters if a specific user ends up on a distro that can't provide webkit2gtk-4.1.

## Files to read when resuming

- `y:/dev/utils/msgx/parity.md` — the authoritative msger feature list
- `y:/dev/utils/msgx/msger/shower.ts` — current msger API surface
- `y:/dev/utils/msgx/msger/msger-plan.md` — msger's independence rule (line 3)
- `y:/dev/utils/msgx/msgview/index.ts` + `main.ts` — see what msgview exposes today
- `y:/dev/email/mailx/bin/mailx.ts:820-900` — mailx's current msger usage
- `y:/dev/email/mailx/client/lib/mailxapi.js` — the in-WebView IPC bridge (full `window.msgapi` surface used by mailx)
- `y:/dev/email/mailx/client/lib/api-client.ts` — IPC vs HTTP auto-detection
- `y:/dev/email/mailx/packages/mailx-service/jsonrpc.ts` — dispatcher (stays unchanged)
- `y:/dev/email/mailx/CLAUDE.md` — architectural rules (msger independence, transport injection)
