QuickMail v0.8.38 Release Notes
Download
There are four downloads. Take a regular one unless you know your PC has an ARM processor — to check, open Settings → System → About and read System type.
| Download | When to use |
|---|---|
QuickMail-0.8.38-win.msi — Windows installer
| Recommended for most users. A standard setup wizard with license agreement; installs per-user with no elevation required, adds the WebView2 Runtime if missing, and enables automatic updates. |
QuickMail-0.8.38-win-arm64.msi — Windows installer, ARM
| The same installer for PCs with an ARM processor, such as the Snapdragon X models of Surface Laptop and Surface Pro. |
QuickMail.exe — standalone portable executable
| No installation required. Copy it anywhere and run. |
QuickMail-arm64.exe — standalone portable executable, ARM
| The portable version for PCs with an ARM processor. |
The regular downloads run on every supported PC, ARM ones included — just not as quickly there. The ARM downloads will not start at all on a non-ARM PC, so if you are unsure, the regular one is the safe guess.
All downloads include the .NET 8 runtime — you do not need to install .NET separately.
New: choose which calendar new appointments start on
If most of your appointments belong on one calendar, you no longer have to steer the Calendar picker away from Local Calendar every time you create one.
In the folder tree, move to the calendar you want — Local Calendar, an account, or one of the calendars beneath an account — open its context menu with Shift+F10 or the Applications key, and choose Use as Default Calendar for New Appointments. From then on the appointment editor opens on that calendar. Clear Default Calendar on the same menu goes back to the local calendar. Both are also in the Command Palette (Ctrl+Shift+P) and can be given keys in Settings → Keyboard.
The calendar you picked is marked (default) in the folder tree and its name is announced as "…, default calendar", so which one is set is something you can check rather than remember.
Two things it deliberately does not do. It does not change which events you are looking at — that is still whatever you have selected in the tree. And it is a starting point, not a rule: the Calendar picker in the editor still sends any individual appointment wherever you want. (#497)
Fixed: the calendar's context menu offered mail folder actions
The context menu on the Calendar node and everything under it was the mail folder menu — New Folder, Move Folder, Set as Archive Folder, Delete Folder. None of them mean anything on a calendar, and every one of them did nothing at all when activated. That menu is now the calendar's own. (#497)
Fixed: Run on Existing Mail has a button again with a Microsoft 365 account
With a Microsoft 365 account, Tools → Rules opens the Rules Manager that works one account at a time, and in v0.8.37 that window had no Run on Existing Mail control — the only way to reach it was Ctrl+Shift+P. It is now a button beside the others, and is also on the rule list's context menu, alongside the Command Palette entry that was already there.
It runs the account shown in the picker, not every account. That is the difference you would expect from a window that shows you one account at a time: it never acts on rules for a mailbox you are not looking at. The Rules Manager you get without a Microsoft 365 account lists every account together and still covers them all, unchanged.
The button is turned off when the account in the picker has no enabled QuickMail rules to run — a mailbox whose rules are all server-side has nothing for it to do, because server rules run in Exchange and cannot be applied to existing mail from here. The outcome is written to the window's status line as well as announced, so it is there to read whether or not you have announcements turned on. (#493)
Fixed: "Show field labels in the rules list" now works with a Microsoft 365 account
The checkbox is in Settings → General for everyone, but only the Rules Manager you get without a Microsoft 365 account was reading it. With one, turning it on changed nothing.
Both windows honor it now. With it on, a rule in the account-at-a-time window reads as "Rule Newsletters, runs on server, status enabled" rather than running the pieces together. The setting is read when the Rules Manager opens, so change it in Settings and then open Rules to hear the difference. (#493)
Fixed: three Settings checkboxes were read out with words that were not their labels
The three Show field labels in the … list checkboxes — contact list, calendar event list, rules list — were each read out with "accessible names" tacked on the end, and the contact one called the list "the address book contact list" rather than what the label says. Each is now read out as its own label. (#493)
Changed: Report a Bug files through a QuickMail service
Help → Report a Bug → Send works exactly as it did — fill in the report, choose Send, and the issue is filed for you. What changed is what happens behind that button.
Until now QuickMail talked to GitHub directly, using a GitHub access token built into the program. A token inside a program you can download is a token anyone who downloads it can pull back out, and every report filed this way arrived on GitHub under the QuickMail author's own account rather than as a report from a user.
Reports now go to a QuickMail service that files them, and that service is the only thing holding any GitHub credential. Issues you send arrive under a QuickMail bot identity. The report itself is unchanged, and still contains no email address or anything else identifying you.
If you are upgrading, QuickMail removes the old credential from Windows Credential Manager the first time it starts. Nothing you need to do. (#501)
Fixed: a meeting invitation opened in its own window had no date or time
With Settings → Windowing set to open messages in their own window, an invitation was missing the card that states when the meeting is and offers Accept, Tentative, and Decline. The card only ever appeared in the reading pane.
This is worse than it sounds, because for many invitations the card is the only place the date and time appear. An invitation sent through Outlook for a Zoom meeting is a body full of join links and dial-in numbers; the actual when lives in the attached calendar part and nowhere else. In window mode there was no date, no time, and no way to answer.
The card now appears wherever a message is opened, and the message window answers invitations too — the three responses are on its command palette (Ctrl+Shift+P) as well as in the card. (#513)
Fixed: pictures in a message lost their descriptions
QuickMail does not load images from the internet, and that has not changed. But it was removing each image so completely that the description the sender wrote for it — the text meant to be read when the picture cannot be seen — went with it. Where a sender had described a picture properly, you got nothing at all.
It was worst where a picture is a link, which is how nearly every newsletter builds the row of social media links at the bottom. Removing the picture left a link with no text in it, so it was announced by its web address instead — and those addresses are tracking links. A row that should read "Facebook link, Twitter link, Instagram link" read as "redirect" three times over.
Descriptions now stay when the picture goes. A picture the sender marked as decorative still contributes nothing, which is what marking it that way asks for. (#163)
Internal
Everything below is developer detail — implementation notes, test coverage, and build changes. Nothing here is needed to use QuickMail.
Calendar
- The default is stored as the tree node's own tail encoding, so there is one parser rather than two.
ConfigModel.DefaultCalendarSourceholds exactly what followsCalendarSourcePrefixon the node the user chose —local,{guid}, or{guid}|{escapedCalId}— andMainViewModel.DefaultCalendarFilterreads it back through the existingCalendarFilterFor. Storing an account/calendar pair instead would have meant a second encoding to keep in step with the tree's, for no gain: the setting is only ever produced by, and only ever refers to, a node in that tree. Empty means no preference, which is distinct from an explicitlocalin the config file but identical in behavior. - Honoring the default is a preselection in
NewEvent, not a change toBuildSaveTargets.EventEditorViewModel.SelectTarget(accountId, calendarId)movesSelectedTargetIndex; the target list itself still puts Local at index 0 unconditionally. That keeps the no-default path byte-for-byte what it was, and keeps the fallbacks in one testable place. The fallback ladder matters because the tree and the picker do not offer the same set: the tree shows a node per discovered calendar for any account with more than one, while only iCloud contributes a target per calendar (Microsoft and Google contribute one, their default calendar). So an exact(account, calendar)match is tried first, then any target on the same account — landing on "that account" rather than on Local, which is a different mailbox entirely — and only a default whose account is gone at all leaves the editor on Local.SelectTargetreturns whether it matched so the fallback is observable in tests rather than inferred. - Calendar nodes now carry
IsCalendarNodeand get their own context menu. TheItemContainerStylesetFolderContextMenuon every tree item including the Calendar subtree, where all five entries fell throughIsMovableFolder's\0guard and silently did nothing. ADataTriggeron the new flag swaps inCalendarContextMenu. The menu is declared inWindow.Resourcesand referenced from the trigger's setter — the XAML-compiler crash the neighbouring comment documents is about declaring a Click-handler menu inside aStyle.Setter.Value, which this does not do. - The marker lives in
AutomationName, not only inItemStatus.FolderTreeNode.IsDefaultCalendarappends ", default calendar" to the accessible name and drives a(default)badge modelled on the unread badge. This is the #227 finding applied to a second piece of state: a marker carried only inItemStatusis not reliably spoken, and a default the user cannot hear is a default they cannot check.PersistDefaultCalendarmoves the marker between existing node objects viaMarkDefaultCalendarNodesrather than rebuilding the tree, which would replace every node and throw keyboard focus out of the item the user just acted on;BuildFolderTreere-applies it after a genuine rebuild. - Both commands are registered, so the context menu is not the only way in.
calendar.setDefaultCalendarandcalendar.clearDefaultCalendarare Calendar-categoryCommandDefinitions with no default key — palette-reachable and rebindable. That is the #250 lesson (folder creation was context-menu-only and therefore undiscoverable without a mouse) applied up front. Refusals are sentences, not silence: the bare Calendar node and All Calendars are not one calendar, andSetDefaultCalendarreturns why, which the View sends to bothStatusTextand aResultannouncement.
Rules
- Run on Existing Mail was the same parity gap as Test Rule, one sweep later, and it carried a scope decision with it. The unified window had the command wired and registered but no button and no context-menu item, so it was palette-only for every Microsoft 365 user — the one the v0.8.37 Test-Rule sweep missed (#493). The mechanical half was the easy half.
RunClientRulesOnExistinghad always built its Inbox map over every account inCachedFolders, which was unambiguous in the client-only window because that window lists every account together; under an account picker, a button directly below Account: Work would have read as "run this for Work" while processing every mailbox.RunOnExistingRequestednow carries aGuid?: the unified window passesSelectedAccount.Idand the shared handler filters the map to it; the client-only window passesnulland keeps its all-accounts run, byte-for-byte unchanged. The scope is enforced where it is load-bearing rather than cosmetically —ApplyRulesToExistingAsynconly ever touches messages whose account is a key in that map, so an unscoped legacy rule reaching an out-of-scope mailbox is not possible. The scope decision then settled theCanExecutequestion filed alongside it: because the run is now exactly the account on screen, "this account has no enabled QuickMail rules" is an unambiguous statement, soCanRunOnExistinggates on it. A Graph mailbox whose rules are all server-side therefore gets a disabled control rather than one that reports "0 moved". The outcome also setsStatusText, not justAnnounce: the sameResult-category reasoning as Test Rule, since a newly surfaced button whose only feedback is an announcement is imperceptible to a user running with announcements off, error included. RuleListShowFieldLabelswas read by one window and ignored by the other. The Show field labels in the rules list checkbox is in Settings for every user regardless of account type, but onlyRulesManagerViewModelread it — so for a Microsoft 365 user the checkbox silently did nothing.UnifiedRuleRownow takes the flag through itsForServer/ForClientfactories and prefixes each field inRowText:Rule Newsletters, runs on server, status enabledrather thanNewsletters, on server, enabled. Read once when the manager opens, matching the client window, so a Settings change applies the next time it is opened. Note the one asymmetry between the two windows: in the client window the setting shapesAccessibleNameonly, whileUnifiedRuleRow.ToString()returnsRowText, so here it changes the visible row text too. The detail pane is untouched either way — it always carries the full definition.
Message rendering
- The event card moved from a call site to the builder, because the obligation was the bug.
MainViewModelbuilt the invite card andMainWindowinjected it, so every other render surface silently shipped without one —MessageWindowhad done so since it existed. Splitting it intoHelpers/EventCardHtmlBuilderand injecting it insideMessageBodyHtmlBuilder.BuildMessageHtml, from the detail's ownCalendarInvite, means no surface can omit it by forgetting. The failure mode is what makes this worth the move rather than a second call: the card is the only rendering of the ICS part, so a surface that drops it shows an invitation with no date and no time at all, and a body that is all join links reads as though the meeting has no when.MessageWindowalso intercepts the card'squickmail:links, guards itsaria-liveconfirmation against having navigated away mid-send, and carries the three RSVP actions in its palette. (#513) - Image alt text is substituted before the removal pass, not preserved by weakening it.
TryStripHeavyHtmldeleted<img>outright; the CSP'simg-src 'none'is what blocks the pixels, so the deletion was discarding the name and buying nothing. The new pass replaces each image with its alt text and leaves the removal pass to take the tag.alt=""and whitespace-only alt are deliberately not matched — that is the author declaring the image decorative, and plain removal is the correct handling — and a missingaltkeeps today's href fallback because there is no name to recover. The value is HTML-decoded and re-encoded rather than spliced: an attribute may hold entities that are already correct as text, but may equally hold a bare<, which is legal inside a quoted attribute and would open a tag once moved into content. The pass fails closed with the rest of the chain, since a partially substituted document is not a sanitized one. The link case is what makes this an accessibility defect rather than a cosmetic one: an anchor whose only content was the image is left empty, an empty link has no accessible name, and WebView2 names it from thehref— which for a newsletter footer is a tracking URL.MessageBodyHtmlBuilderTestspins the icon-only-link shape against a real captured example, plus the three no-useful-alt shapes, entity round-tripping, markup in an alt, and the timeout. Message-list preview text is unaffected: it comes from the server (IMAPPREVIEW/ GraphBodyPreview), not from this path. (#163) - Loading remote images at all is deliberately still not offered. (#508) tracks the opt-in — per message, per sender, and the
img-srcrelaxation confined to that path. Alt text is the right fix independent of it: it is what names an image whether or not the pixels ever load.
Bug reporting
- The capability moved out of the binary; the key that stayed in it was reduced to something worth extracting nothing for. Release builds compiled a fine-grained GitHub PAT into the single-file executable and posted to
api.github.comwith it, so anyone who downloaded QuickMail could pull the token out, and GitHub attributed every user-filed report to the token owner (#222). A GitHub App now owns the write capability with its private key held by a Cloudflare Worker (relay/). The client posts to the Worker with a relay key that still ships in the binary and is still extractable — the difference is that its whole authority is "file an issue on kellylford/QuickMail", it is rate-limited at the relay, and it rotates without touching a GitHub account. The client also stopped sending its own labels: a client that names them lets anyone holding the extracted key apply arbitrary ones, so the relay decides labels. Upgrading installs delete the pre-relay PAT from Credential Manager on first run. - The PKCS#1 → PKCS#8 conversion is tested against a real Node export, byte for byte. GitHub hands out App keys as PKCS#1 and WebCrypto imports only PKCS#8, so a wrong conversion fails at runtime with an opaque error and nothing to bisect.
relay/test/jwt.test.jsasserts identical DER rather than "it parsed". docs/BUG-REPORTING.mdis the runbook — rotating the relay key, reading Worker logs, and what to check when reports stop arriving.relay/README.mdrecords the workers.dev subdomain step that broke the first deploy.
Reporting Issues
Found a problem or have a suggestion? There are three ways to reach us — pick the one that fits:
- Report a Bug → Send (Help menu, inside QuickMail). Files the report for you anonymously — it includes no email address or other identifying information, so there is no way to follow up with you. Best when you don't want any follow-up.
- Report a Bug → Copy report and open GitHub (Help menu). Opens a pre-filled issue that you submit under your own GitHub account, so your GitHub contact information is attached. Best when you have a GitHub account and want automatic filing plus direct contact.
- Email quickmailissues@theideaplace.net. Best when you don't mind sending email and want a personal follow-up.
Full details, including exactly what a report contains (and what it never contains), are in the Reporting Issues section of the User Guide.