# Installer & Automatic Updates QuickMail is packaged with [Velopack](https://velopack.io/). `build.bat installer` runs `dotnet publish` then `vpk pack`, emitting `installer/Output/Releases/` (gitignored) containing: - `QuickMail-win.msi` — **the user-facing installer**: a standard Windows Installer wizard (WiX 5) with welcome, license acceptance, and conclusion pages fed from `installer/velopack/*.txt` and the repo `LICENSE` - `QuickMail-win-Setup.exe` — Velopack's one-click installer (no wizard, no license page); produced by `vpk pack` but **not shipped** — the MSI is the installer users download. It was briefly shipped for winget (issue #536) and that was **reverted before any release carried it**. Do not ship it again without reading *Setup.exe over an MSI install* below: running it on top of an existing MSI install leaves the machine with two Add/Remove Programs entries, and removing the wrong one deletes the working install. - `QuickMail--full.nupkg` — the full update package consumed by the in-app updater - `QuickMail--delta.nupkg` — binary delta from the previous release (generated only when the previous release's packages are present; CI fetches them with `vpk download github` before packing, local builds produce full packages only) - `RELEASES`, `releases.win.json`, `assets.win.json` — release feed metadata read by the updater - `QuickMail-win-Portable.zip` — **not shipped**; the raw single-file `publish/QuickMail.exe` remains the portable download The `vpk` CLI is a .NET global tool. CI installs the version pinned in `.github/vpk-version` (`dotnet tool install -g vpk --version `); match it locally. It was unpinned through 0.8.52, and moved from 1.2.0 to 1.2.158 between 0.8.47 and 0.8.48 without anyone noticing. To bump it: run the *Winget install-path matrix* workflow with the new version as its `vpk_version` input, re-check the channel filename listing below against that run's Scenario 0, then edit the file. The `Velopack` NuGet package the app references is a separate pin (`QuickMail/QuickMail.csproj`). Keep it at the same version as the CLI — `VelopackVersionPinTests` fails when they differ. When the library is older, packing logs `Velopack library version is lower than vpk version`; when it is **newer**, vpk logs an error it says may become fatal, so never bump the package (Dependabot will propose it) without the CLI. Releases 0.8.48 to 0.8.53 shipped with library 1.2.0 under CLI 1.2.158/1.2.161 and self-updated normally; from 0.8.54 the library is 1.2.161 too. Bumping the package is an app change, so give it the update-cycle test (*Testing updates locally* below) across the transition — from the last release's library to the new one, and from the new one to itself. The 1.2.161 bump was tested that way on ARM64 in the test VM, with a local `--updateFeed` and the update applied on exit: 0.8.53 updated to a 1.2.161-library build, which then updated to the next. ## Two architectures, two channels Releases ship both an x64 and a native ARM64 build (issue #18). They are packed into the same output folder but on **separate Velopack channels**, because a single channel serving two architectures would offer each build's update to the wrong machine. | | x64 | ARM64 | | --- | --- | --- | | Channel | `win` (Velopack's default) | `win-arm64` | | Pack arguments | none extra | `--runtime win-arm64 --channel win-arm64` | | Installer | `QuickMail--win.msi` | `QuickMail--win-arm64.msi` | | Portable | `QuickMail.exe` | `QuickMail-arm64.exe` | | Feed metadata | `RELEASES`, `releases.win.json`, `assets.win.json` | `RELEASES-win-arm64`, `releases.win-arm64.json`, `assets.win-arm64.json` | | Package | `QuickMail--full.nupkg` | `QuickMail--win-arm64-full.nupkg` | **`win` means x64 and must never be renamed.** Velopack's own guidance is to migrate the default channel to `win-x64` when adding a second architecture, but every installed copy of QuickMail already polls `releases.win.json` — renaming it would strand all of them on their current version permanently. The naming is inconsistent on purpose. Velopack suffixes every file it writes with the channel name, so both channels can safely share one output folder and one GitHub release. That was verified against real vpk 1.2.0 output (run 30710511730) and the listing is reproduced in the workflow comment above the ARM64 pack step. The unnoticed move to 1.2.158/1.2.161 was checked after the fact: the asset names of 0.8.52 are identical to 0.8.47's, and installed copies updated across it. **Re-verify it after any vpk upgrade** — a filename collision would overwrite the x64 feed and strand every existing install. The only genuine collision is the portable executable, which is uploaded straight from the publish folder rather than through vpk: both architectures produce a file named `QuickMail.exe`, so the ARM64 copy is renamed to `QuickMail-arm64.exe` before upload. **No cross-architecture migration exists.** An installed x64 copy running on an ARM64 device is never offered the ARM64 build, and vice versa — the installed copy records its channel and checks only that one. Switching is a manual uninstall and reinstall. Settings survive: the profile lives in `%APPDATA%\QuickMail` while Velopack installs to `%LocalAppData%\QuickMail`, and credentials live in Windows Credential Manager. Neither is touched by uninstalling the other architecture's build. Local ARM64 publish: `build.bat publish-arm64` (cross-compiles on x64, output in `publish-arm64/`). Note that both architectures share `bin/` and `obj/`, so an ARM64 publish leaves ARM64 output in `bin/Release` until the next ordinary build. ## Key facts - **Per-user install only (deliberate).** The MSI is packed with `--instLocation PerUser`: installs to `%LocalAppData%\QuickMail\current\`, no elevation prompt. Velopack supports `--instLocation Either` (user chooses Program Files), but background updates from a non-elevated process into Program Files are unverified — offering that choice could break the silent-update guarantee, so it stays off until proven. - **Automatic updates.** The startup update check (`UpdateCheckService`) uses Velopack's `UpdateManager` with a GitHub Releases source when running from an installed copy. A found update is downloaded silently in the background and applied by `Update.exe` after the app exits, so the next launch runs the new version. Updates work identically whether the MSI or Setup.exe performed the original install. The portable exe falls back to the original GitHub API check, which can only notify. - **Desktop shortcut is the app's job, not the installer's.** `--shortcuts StartMenuRoot` creates only the Start Menu entry. On first launch of an installed copy, QuickMail offers to add a desktop shortcut (accessible in-app dialog, default No, asked once — recorded as `DesktopShortcutPrompted` in config.ini); the choice stays editable in Settings → General. The shortcut targets `%LocalAppData%\QuickMail\current\QuickMail.exe`, which is stable across updates. See `Helpers/DesktopShortcut.cs`. - **Uninstall offers to remove user data.** Velopack's `OnBeforeUninstallFastCallback` (wired in `App.xaml.cs Main`) launches a detached PowerShell prompt — detached because Update.exe kills hook processes after 30 seconds, far too short to leave a question pending. Default answer keeps everything; an explicit Yes deletes `%APPDATA%\QuickMail` **and** QuickMail entries in Windows Credential Manager (passwords `QuickMail:`, tokens `QuickMail.*`). Custom `--profileDir` locations are never touched. Data survival on uninstall-without-Yes matches the old Inno behavior. The prompt is **best-effort**: on script-restricted machines (AppLocker, Constrained Language Mode) it may never appear, in which case data is simply kept — the safe default. The hook and the prompt script log to `%TEMP%\quickmail-uninstall.log` for diagnosis. - **WebView2 install-on-demand** is preserved via `--framework webview2` — setup installs the WebView2 Runtime when missing, same as the old Inno `Dependency_AddWebView2`. - **Version string must be SemVer.** `vpk pack --packVersion` rejects 4-part versions. The release tag is the source of truth in CI (stripped of the leading `v`) and must match the csproj `` — the workflow fails the release if they differ. A hotfix should use a SemVer2 prerelease tag (e.g. `0.8.1-1`), not a 4th segment. - **Entry point requirement.** `vpk pack` verifies via IL inspection that `Main` calls `VelopackApp.Build().Run()`. That is why `App.xaml` compiles as `Page` and `App.xaml.cs` declares an explicit `Main` (see the csproj `StartupObject`). Removing or reordering that call breaks packaging. - **Code signing** runs in CI on every tagged release through Azure Trusted Signing: `vpk pack --azureTrustedSignFile` signs the packaged binaries, `Update.exe`, `Setup.exe` and the MSI as they are produced, and a separate step signs the two portable exes. There is no signing certificate or long-lived secret in the repo — the workflow's OIDC token is exchanged for Azure credentials via `azure/login`. Consequence: **nothing may edit a packed MSI or exe after `vpk pack`** (a post-pack transform would void the signature). - **A silent MSI install goes to `%LocalAppData%\QuickMail`, from 0.8.48 on** (issue #554). The MSI is the only installer shipped, so it is also what an unattended deployment — Intune, Group Policy, a script, winget — runs. Releases up to 0.8.47 were packed with vpk 1.2.0, whose MSI set the per-user default only from the wizard's Next button, so `msiexec /i … /qn` fell back to a drive root (`C:\QuickMail` on one machine, `D:\QuickMail` on a CI runner). vpk 1.2.158 fixed it upstream (velopack/velopack#945, PR #970: a quiet install, UI level below 5, now defaults to `%LocalAppData%`). Measured on both architectures with vpk 1.2.161 (install-matrix run 37268689330, Scenario 1): one install directory, `%LocalAppData%\QuickMail`, and one visible Add/Remove Programs row — the same as a wizard install. For an MSI from 0.8.47 or earlier, pass the location yourself: ```bat msiexec /i QuickMail--win-arm64.msi /qn VELOPACK_INSTALLDIR="%LocalAppData%\QuickMail" ``` What did not change: a newer MSI over an older one is still a Windows Installer major upgrade that uninstalls the old copy first (#245; measured again in install-matrix scenario 6), and Velopack runs the uninstall hook for it. From 0.8.53 the hook copes: the data prompt waits until Windows Installer is idle and asks only if QuickMail is still gone (`Helpers/UninstallDataPrompt.cs`), and the start-at-sign-in entries it removes are handed to the new copy's install hook, which restores them (`Services/StartupEntryHandoff.cs`). Copies older than 0.8.53 still run the old hook when they are the ones being replaced. A side effect worth knowing: uninstalling and reinstalling within 15 minutes also restores start at sign-in, since the hand-off cannot tell that from an upgrade. Install-matrix scenarios 6 and 6b check both halves on real MSIs: across an upgrade the startup entry comes back with its Task Manager mark, and a genuine uninstall still asks (about 11 s after removal). The upgrade's file gap measured about a second, and Windows Installer's `Global\_MSIExecute` mutex is held across all of it (x64: held 1.9-10.8 s, files gone 3.4-4.8 s; ARM64: held 1.8-24.8 s, gone 20.4-21.0 s), so the files coming back, or never being seen gone, decide an upgrade, the mutex covers a slower one, and the Add/Remove Programs check is behind both. The in-app updater is still the upgrade path; the MSI is for first installs. `Setup.exe --silent` installs to the same place and overwrites in place without the uninstall hook, which is why winget was first going to use it. It is not shipped, and the bullet below is why. - **`Setup.exe` over an MSI install leaves a booby-trapped Add/Remove Programs entry.** This is why `Setup.exe` is not a release asset, and it is not specific to winget: it applies to anyone who runs `Setup.exe` on a machine where QuickMail was installed from the MSI. Measured on both architectures by `.github/workflows/winget-install-matrix.yml` (scenarios 2, 6 and 7); full results in `docs/planning/winget-distribution-plan.md`. Velopack writes its own uninstall entry under the key `QuickMail`, while an MSI install registers under `MSI:QuickMail`. Setup.exe does not remove the MSI's registration, so the machine ends up with **two entries both named QuickMail** — and both are dangerous to act on: - Removing the **stale `MSI:QuickMail` entry deletes the working install.** Windows Installer removes the files it recorded, which Setup.exe has overwritten in place: the install root, `Update.exe`, `QuickMail.exe` and everything under `current\` are gone, in about a second, exit code 0. The stale entry shows the *older* version number, so it is the one a tidy-minded user removes. - Removing the **live `QuickMail` entry leaves the stale one behind**, still listed in Settings → Apps, along with the hidden HKLM row and the Windows Installer product registration — so the app appears still installed after being uninstalled. The only clean sequence is uninstall-then-install, never install-over. Until Velopack cleans up its own MSI registration (or QuickMail does it in the install hook), shipping `Setup.exe` hands users a way to destroy their own install with no warning. ## Release flow (CI) On a `v*` tag, `.github/workflows/quickmail.yml`: 1. Verifies the tag version matches the csproj ``. 2. `dotnet publish` (unchanged single-file self-contained build). 3. `vpk download github` — fetches the previous release's packages so a delta can be built. Allowed to fail (the first Velopack release has no prior packages). 4. `vpk pack` — builds setup exe, MSI, full/delta packages, and feed metadata. The previous version's full `.nupkg` fetched in step 3 stays in the folder and uploads with the rest: `vpk pack` writes an entry for every package it saw into `RELEASES`/`releases.win.json`, so deleting it would publish feed metadata pointing at an asset the release does not carry. An old-version `.nupkg` on a release page is expected, not a mistake. 5. `softprops/action-gh-release` uploads the portable `QuickMail.exe`, the MSI installer, the `.nupkg` packages, and the feed metadata files — for both architectures. The in-app updater reads these from the latest GitHub release. `Setup.exe` and `Portable.zip` are produced by `vpk pack` and deliberately not uploaded. ## Testing updates locally (no GitHub release needed) The `--updateFeed ` startup flag points the update check at a local folder of `vpk pack` output instead of GitHub Releases, so the complete cycle — check, background download, apply on relaunch, delta packaging — can be verified offline: 1. `build.bat installer` — packs the current csproj version (say `0.8.1`) into `installer\Output\Releases\`. 2. Run `installer\Output\Releases\QuickMail-win.msi`. Walk the wizard (welcome, license, conclusion); the app installs to `%LocalAppData%\QuickMail`. First launch offers the desktop shortcut. 3. Bump `` in `QuickMail/QuickMail.csproj` (say `0.8.2`) and run `build.bat installer` again **without deleting the Releases folder** — because the previous full package is still there, this also exercises delta generation (`QuickMail-0.8.2-delta.nupkg` appears). 4. Launch the **installed** copy with the feed override: `%LocalAppData%\QuickMail\current\QuickMail.exe --updateFeed \installer\Output\Releases` The startup check finds 0.8.2, announces it, and downloads it in the background (`quickmail.log` records "Update 0.8.2 downloaded; it will be applied when QuickMail exits"). 5. Exit QuickMail. `Update.exe` applies the staged update. 6. Relaunch (Start Menu or the same command). Help menu now reads "running version 0.8.2". Add `--profileDir ` to any of these launches to keep test runs away from real data. Uninstalling via Settings → Apps removes `%LocalAppData%\QuickMail` and shortcuts; revert the csproj version bump afterwards. ### The same cycle on ARM64 Run steps 1–6 on an ARM64 machine using the ARM64 packs. `build.bat installer` is x64-only, so produce the ARM64 packs the way CI does — `build.bat publish-arm64` followed by a `vpk pack` carrying `--runtime win-arm64 --channel win-arm64 --packDir publish-arm64` — or take a run of the **Build Installer (on demand)** workflow with `architecture: arm64`, which also gets you real OAuth credentials. Two things this cycle proves that the x64 one does not: - An ARM64 install finds and applies an ARM64 update, i.e. Velopack resolves the `win-arm64` channel from the installed copy's own metadata without the app passing a channel explicitly (`UpdateCheckService` constructs `UpdateManager` with no channel). - With both channels' output in one folder, an ARM64 install is **not** offered the x64 package and an x64 install is **not** offered the ARM64 one. This is the check that matters: getting it wrong installs an executable that will not start. The flag only overrides *where packages come from*; everything else (Velopack's installed detection, staging, `Update.exe` apply) runs exactly the production code path. What it cannot test: the GitHub Releases fetch itself (`GithubSource`) and the CI packing steps — those are exercised by the first real tagged release. ## Testing the live GitHub update path (one-time, two real releases) The local `--updateFeed` cycle above proves everything except the one link that only runs in production: Velopack's `GithubSource` resolving the latest release and downloading its assets from GitHub. That link cannot be exercised until **two** published releases exist, because there is nothing for the first release to update *to*. Do this once — it proves the update channel for every future release. A broken updater is self-perpetuating (stranded clients cannot be auto-fixed), so verify it before you ever rely on it to ship an urgent fix. Run it on your own machine before trusting it broadly: 1. **Publish the baseline (0.8.0).** Confirm the tag equals all three csproj version properties, then push the tag: `git tag v0.8.0 && git push origin v0.8.0`. CI builds and creates the release. 2. **Verify the release assets by eye.** On the GitHub release page, confirm all four updater files are attached: `QuickMail-0.8.0-full.nupkg`, `RELEASES`, `releases.win.json`, `assets.win.json` (plus the `QuickMail-win.msi` and portable `QuickMail.exe`). A missing or malformed feed is the most likely failure and the one that strands clients *silently*. 3. **Install the baseline from the real release.** Download and run `QuickMail-win.msi` from the release page (not a local build) so the installed copy is exactly what users get. 4. **Publish an update (0.8.1).** Bump ``, ``, and `` in `QuickMail/QuickMail.csproj`, add `docs/release-notes-v0.8.1.md`, commit, then `git tag v0.8.1 && git push origin v0.8.1`. The 0.8.1 release **must be a normal published release, not a draft or prerelease** — the client runs `GithubSource(..., prerelease: false)` and will not see a prerelease tag. 5. **Watch the installed 0.8.0 update itself.** Launch the installed copy **with no `--updateFeed` flag** (add `/debug` to get log detail). The startup check should find 0.8.1 and download it in the background — `quickmail.log` records `Update 0.8.1 downloaded; it will be applied when QuickMail exits`. Exit, relaunch, and confirm Help → About reads 0.8.1. 6. **If nothing happens:** re-launch with `/debug` and read `quickmail.log`; failure reasons are logged via `LogService.Debug`. Then recheck step 2 — a bad feed file is the usual cause. **Do not delete/reuse the test 0.8.1.** Deleting a published release your test machine already updated to leaves that machine pointing at gone assets, and reusing the `0.8.1` number later muddles the feed metadata and delta chain. Just keep 0.8.1 as a legitimate (even trivial) release and let your next work be 0.8.2. The first *delta* package is only exercised on the 0.8.1 → 0.8.2 hop; the 0.8.0 → 0.8.1 update ships a full package, which is expected. ## Migrating from the Inno Setup installer Users on an Inno Setup install (v0.7.9.1 and earlier) need a one-time manual reinstall to get onto the auto-update track: 1. Uninstall QuickMail via Settings → Apps, **declining** the offer to delete user data. 2. Download and run `QuickMail-win.msi` from the releases page. 3. Launch QuickMail — accounts, settings, and mail are exactly as they were. The retired Inno Setup script (`installer/quickmail.iss`, `installer/CodeDependencies.iss`, `installer/Languages/Custom.en.isl`) is kept in the repo for reference until the first Velopack release has shipped successfully.