QuickMail v0.8.33
QuickMail v0.8.33 Release Notes
Download
Two options are available for v0.8.33:
| Download | When to use |
|---|---|
QuickMail-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.exe — standalone portable executable |
No installation required. Copy it anywhere and run. |
Both downloads include the .NET 8 runtime — you do not need to install .NET separately.
This is a big release. QuickMail now has a full calendar: create your own appointments, keep repeating events, get reminders, respond to meeting invitations, and connect your Microsoft, Google, or iCloud calendar. It also makes new-mail sync more reliable, quiets down notifications after your computer wakes, and fixes opening a message in a tab. See the Calendar section of the User Guide for the full walkthrough. If you installed QuickMail from the MSI, this update is delivered automatically.
New: The Calendar
QuickMail now has a real, keyboard-first calendar. Press Ctrl+Shift+C (or select the Calendar node in the folder tree) to open it. Move through your events with the Up and Down arrows; on any appointment, press Tab once to reach a details box with everything about it, or Enter to open the full appointment.
The Calendar page of the User Guide covers all of this in detail. In brief:
Create and manage your own appointments
Press N to create an appointment. You get a title, all-day or timed start and end (type times naturally — "9", "9:00 AM", "14:30"), location, notes, and repeat options. Press E to edit and Delete to delete; QuickMail always confirms before deleting.
Repeating appointments support Daily, Weekly, Monthly, and Yearly patterns, an "every N" interval, an optional end date, and — for weekly events — a day-of-week picker. When you edit or delete one occurrence, QuickMail asks whether you mean this event only or the whole series.
Four views and quick date navigation
Switch between Agenda, Day, Week, and Month views with A, D, W, and M. Move between periods with Ctrl+Left and Ctrl+Right, jump to today with T, and jump to any date with Ctrl+G (Go to Date). In Month view, arrow keys move day by day and Enter opens the selected day.
Reminders
Turn on Remind me before appointments in Settings → General and choose how many minutes ahead you want them. QuickMail then shows a Windows notification and announces each reminder as it comes due. Reminders are off by default.
Meeting invitations
Open an email that contains a meeting invitation and QuickMail adds an event card to the top with Accept, Tentative, and Decline buttons. Choosing one replies to the organizer and updates your calendar right away. Cancellations remove the matching entry automatically.
You can also respond straight from the calendar: an invitation you haven't answered shows as Pending, and pressing Enter on it opens a short menu — Accept, Tentative, Decline, or Open full appointment — so you can reply without leaving the calendar to find the email.
Connect an online calendar — per account
Calendars connect per account, just like contact sync. When you add an account — or later, in Manage Accounts — check Sync calendar from this account and QuickMail shows that account's calendar:
- Microsoft (Outlook.com, Microsoft 365) and Google — see your events, and create, edit, or delete single (non-repeating) events, with the change sent back to that account.
- iCloud — see your events, and create, edit, or delete single (non-repeating) events, with the change sent back to your iCloud account. QuickMail uses the app-specific password you already entered for the account, so there's nothing else to set up.
Nothing syncs until you check the box, and unchecking it removes that account's events. Connected calendars refresh in the background; F5 (or Sync Calendars Now in the command palette) refreshes on demand. Repeating appointments are always saved to your local calendar, and repeating events that come from an online calendar are read-only for now — the guide's what the calendar does and does not do list lays out the current limits.
Search and export
Press Ctrl+Shift+S to search your appointments by title, location, or notes. Export any appointment as a standard .ics file from the command palette.
Feedback welcome
Feedback on the calendar user experience and any issues you encounter with calendar management is welcome. Testing across a range of appointment types and calendar accounts has been conducted but this is a large surface area. Please see the Reporting Issues section for ways to report issues in general or provide feedback on the calendar experience.
New: More reliable new-mail sync
QuickMail primarily learns about new mail through a live server connection (IMAP IDLE). This release adds a safety net behind that connection and fixes two related reliability problems:
- A periodic new-mail check. If a server never pushes, a held connection dies quietly, or you read or flag messages in another app, QuickMail now re-checks your inboxes on a schedule. Set it in Settings → General, under Sync, at "Check for new mail every" (Off, 1, 5, 15, 30, or 60 minutes; 5 by default). Changes apply without a restart.
- No more "connection was forcibly closed" after idle. Pooled connections that have been idle for a while are now checked and reconnected before reuse, so opening a message after your machine has been sitting a while no longer fails.
- Read state stays in sync. A message you read in another client (for example, Gmail on the web) now stops showing as unread in QuickMail.
New: Quieter notifications after your computer wakes
When your computer wakes from sleep or a dropped connection reconnects, all the mail that arrived during the gap arrives at once. Previously that produced a single large "9 new messages" burst notification, and it could repeat on each wake. QuickMail now recognizes a catch-up backlog (more than a handful of messages in one batch) and skips the toast for it — the messages are still marked as seen so nothing re-fires. Real-time arrivals, a few at a time, notify normally.
Fixed: Opening a message in a tab now shows just the message
When you set messages to open in a tab (Settings → Windowing → Reading mode → Tab), activating a message opened a tab that showed a copy of the whole message list with the message itself squeezed into a small, fixed strip at the bottom — not the message on its own. Opening in a window worked correctly.
Tabs now behave as expected: opening a message in a tab fills the pane with that message, and the message list is set aside until you return to it. The message list is still there whenever you need it —
- Escape from an open message returns you to the message list (the message's tab stays open).
- Ctrl+W closes the message's tab and returns you to the list.
- The tab strip (Ctrl+Shift+T, or F6 to reach it) lets you switch between the list and any open messages.
The list also stays visible while a message is still loading, and if a message fails to load the list remains on screen rather than leaving the pane blank.
Fixed: The compose preview window no longer opens blank
Previewing the formatted (Markdown) version of a message you are composing opened a blank window on current WebView2 runtimes. The preview now renders your formatted message as expected.
Thank You to Contributors
Thank you, as always, to everyone who contributes to QuickMail through code, bug reports, feature suggestions, and other feedback — including the reports that shaped the calendar's design, the tab-open behavior found during the theming review, and the notification and sync-reliability reports.
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.
Internal
Full calendar (PR #273, spec docs/planning/full-calendar-pm-dev-spec.md)
- Local authoring with a modeless
EventEditorWindow(per the modal-dialog rules — editable fields over the live WebView2), master/detail list + details pane, and four views (Agenda/Day/Week/Month) driven byCalendarViewModel.ViewMode+ReferenceDate. Month is a 42-cell grid with arrow navigation and Enter-to-drill. - Recurrence is a practical RFC-5545 subset (
RecurrenceRule,RecurrenceExpander):FREQdaily/weekly/monthly/yearly,INTERVAL, weeklyBYDAY, and oneCOUNT/UNTILend condition. Expansion is local wall-clock (DST-safe) with a ~10-year iteration cap. Per-occurrence edit/delete usesEXDATE+ detach; whole-series edits preserveEXDATEs and the original start. - All-day events are fully supported and persisted (
is_all_daycolumn; re-anchored to local midnight across all providers to avoid off-by-one-day display). The "timed events only / deferred" comment inEventEditorViewModelis stale and should be removed. - Times stored as UTC ticks; TZID handling per provider (
Prefer: outlook.timezonefor Graph, RFC3339 offsets for Google, kind-carrying stamps for CalDAV). - Sync engine (
GraphCalendarSyncService, name predates multi-provider): read-down window −30…+365 days, replace-slice per account, background pass every 15 min plus one after startup mail sync, best-effort (never throws),silentOnly(no interactive sign-in). Write-back is single events only for Microsoft, Google, and iCloud (recurring push rejected pre-network withNotSupportedException); iCloud CalDAV write (CalDavCalendarClient) was added in a later PR — see iCloud calendar write below. Failed server create/edit falls back to a local save, announced. Which accounts sync is the per-account opt-in below. - Folder tree:
Calendarsentinel node withAll Calendars/Local Calendar/ one child per opted-in account, and — when an account has more than one calendar — a grandchild per calendar.CalendarFilterFormaps a node toCalendarViewModel.SourceFilter(aCalendarFilter(Guid? Account, string? CalendarId)record). - Invitations:
LocalCacheCalendarProviderharveststext/calendarparts; Accept/Tentative/Decline event card in the reading pane sends an ICS REPLY and upserts the response status immediately;METHOD:CANCELmarks events cancelled (filtered from all views). - Reminders: opt-in 60-second timer (
CalendarReminders/CalendarReminderMinutes, default off / 10 min), Windows notification +AnnouncementCategory.Result, fired at most once per(uid, start)per run. - Export via
IcsModel.ExportEvent(calendar.exportEvent, no default key). - Settings: Calendar field labels; Calendar reminders + minutes.
ShowDeclinedEventsremains config-only (read at construction; not live-updated).
Per-account calendar sync (#282)
- Calendar sync is now a per-account opt-in mirroring contact sync (#256), not a global setting. New
AccountModel.SyncCalendarbool (auto-persists); a "Sync calendar from this account" checkbox in the Add Account and Manage Accounts dialogs (AccountEditorViewModel.ShowCalendarSyncOption/AccountManagerViewModel.CanSyncCalendar+SetCalendarSyncAsync, immediate-apply via aClickhandler). Offered for Microsoft, Google, and iCloud — a superset of contacts. GraphCalendarSyncService.SyncAllAsyncnow gates onSyncCalendar; new publicSyncAccountCalendarAsync/RemoveAccountCalendarAsyncdrive the enable/disable paths.IsCalendarPushAccount(save-target picker) and the calendar tree loop also gate on the flag.- iCloud calendars are reached over CalDAV per account (
https://caldav.icloud.com), reusing the account's own app-specific password (ICredentialService.GetPassword(account.Id)) — the same one IMAP uses, no separate credential.CalDavCalendarClient(manual-redirect discovery + REPORT fetch) is retained and adapted per-account; the discovery result is cached per account id. - Consent: Microsoft gets a one-time
RequestCalendarConsentAsync(Calendars.ReadWrite) at opt-in; Google's calendar scope is already granted at mail sign-in; iCloud needs none. TheOAuthRouterroutes only Microsoft to the consent call. - Removed the interim global Settings → Internet Calendar (CalDAV) source: its UI,
SettingsViewModelCalDav members,[caldav]config keys, and the synthetic-id (AccountIdFor/SecretKeyFor) helpers — all superseded by the per-account model. - New palette command
calendar.syncNow("Sync Calendars Now"). Tests: per-account CalDAV sync (rewrittenCalDavCalendarSyncTests), opt-in gating,ShowCalendarSyncOption,IsCalendarPushAccount.
Multiple calendars per account (separate & selectable)
- Each account's calendars (Home / Family / Work …) now sync and appear as separate selectable nodes under the account in the folder tree, instead of only the primary/default calendar.
CalendarEventgainsCalendarId/CalendarName(two idempotentALTER TABLEmigrations, no schema-version bump); every event is tagged with its source calendar. - All three providers enumerate every calendar and union them into the existing per-account replace-slice (one
ReplaceGraphCalendarEventsAsync(account.Id, union)): MicrosoftGET /me/calendars→ per-calendarcalendarView; GooglecalendarList→ per-calendar events; iCloud CalDAV keeps all VEVENT collections (discovery cache is now a per-account list) and issues one REPORT per calendar. A single calendar's fetch failing leaves the prior slice intact. - Tree grandchildren (only when an account has >1 calendar) are fed from a new
LocalStoreService.LoadCalendarSourcesAsync(DISTINCT account/calendar), cached and refreshed on startup and after each sync. Selecting a calendar node filters to that calendar; the account node still shows all its calendars merged.
iCloud calendar write (CalDAV)
- iCloud calendars are no longer read-only: you can create, edit, and delete single (non-repeating) appointments on them, alongside the existing Microsoft/Google write-back. When creating an appointment, the save-target picker lists each of your Apple calendars (e.g. "Apple: Home", "Apple: Family") so you choose which one.
CalDavCalendarClientgainsPutEventAsync/DeleteEventAsync(the redirect/Basic-authSendAsyncrefactored into a sharedSendCoreAsyncso reads and writes share it); PUT sendstext/calendarwithIf-None-Match: *on create; the VCALENDAR body omitsMETHODper RFC 4791.GraphCalendarSyncServicecreate/edit/delete gain an iCloud branch;IsCalendarPushAccountincludes iCloud.- Read-sync now stores each event's real CalDAV resource URL (
CalendarEvent.ResourceUrl, newresource_urlcolumn), so editing or deleting an event created on another device targets the correct server resource (Apple names resources by a random filename, not the UID). Recurring events stay read-only; last-write-wins (no ETag/If-Match in v1).
iCloud contacts via CardDAV
- The "Sync contacts from this account" checkbox (#256) now also appears for iCloud accounts, alongside Microsoft and Google — so every account offers a checkbox for each service QuickMail can read. iCloud contacts sync over CardDAV (
https://contacts.icloud.com) using the account's own app-specific password (GetPassword(account.Id)), no OAuth. - New
CardDavContactClient(mirrorsCalDavCalendarClient: manual-redirect Basic-authSendAsync,current-user-principal→addressbook-home-set→ collections discovery,addressbook-queryREPORT) + minimal vCard parsing (VCardModel, reusingIcsModel's unfolding/block-scan/unescape).ICloudContactSource : IProviderContactSource(ContactSource.ICloud) plugs intoContactSyncServicevia an iCloud host guard inSourceFor.ShowContactSyncOption/CanSyncContactsextended to the iCloud host; consent is requested for Microsoft and Google only (iCloud reuses the app password). iCloud contacts are saved contacts only (CardDAV has no "recent recipients" concept).
Go to Date (PR #279)
calendar.goToDate(default Ctrl+G, rebindable, in the Command Palette) opens a modelessGoToDateWindow(DatePicker). Day/Week/Month keep their view and recenter; Agenda switches to Day.RequestGoToDateannounces unavailability in online mode; the picker is single-instance.
Mail-sync reliability (PRs #271, #272, split from #240)
- #267 (PR #272):
ConfigModel.MailSyncPollMinutes(default 5; 0 disables; clamped 1–120) drivesStartFallbackSyncAsyncinMainViewModel, a periodic inbox re-sync behind IMAP IDLE. New Settings → Sync control; re-reads the interval each cycle so changes apply without a restart; notifies through the shared de-dupe path. - #268 (PR #271): NOOP-probe pooled IMAP connections idle beyond 30s before reuse, fixing "An existing connection was forcibly closed by the remote host" when opening a message after a long idle.
- #269 (PR #271): reconcile
\Seenfor already-cached messages on folder sync, so a message read in another client stops showing unread. - #270 (PRs #271, #274): the new-mail notify breakdown (incoming/unread/fresh counts and UIDs) logs at Log level when a toast actually fires (
fresh > 0), so users who enable logging via Settings → Advanced (Log, not Debug) can capture it; the frequentfresh == 0evaluations stay Debug-only.
Wake/reconnect toast suppression (PR #275)
- When one notify evaluation yields more than
MaxNotifyBatchSize(5) genuinely-new messages, treat it as a catch-up backlog and skip the toast (messages still marked notified so they don't re-fire; count logged). The startup backlog was already excluded by the notify threshold; this is its mid-session equivalent for wake/reconnect bursts.
Tab open-mode showed the message list instead of the message (issue #177, PR #264)
- In
MessageOpenMode.Tab, the right-pane content region left the message-list container visible (it was theDockPanelfill child) while the reading pane showed as aMinHeight=200Dock=Bottomsliver, so a message tab rendered the list plus a body strip rather than the message alone. Window mode was unaffected (separateMessageWindow); Reading-Pane mode was unaffected. - The content region is now a two-row
Gridwhose row sizes swap on a newIsMessageListAreaVisibleVM flag viaBoolToGridLengthConverter. The flag is!(MessageOpenMode == Tab && ActiveTab is MessageTabViewModel && IsMessageOpen)— theIsMessageOpenterm keeps the list visible during the async body load and if the load fails/returns null, so a slow or failed open never blanks the pane (Feature Checklist rule 4).CycleFocusAsyncgates the message-list F6 stop on the same flag; the window-level Escape handler routes toActivateMessageListTab()(revealing the list, leaving the tab open) in Tab mode instead ofCloseReadingPane. - Reading-Pane and Window layouts are byte-for-byte equivalent (the flag is always true outside Tab mode). Independent review caught and fixed the transient/failed-load blank-pane case before merge. Adds
TabModeMessageListVisibilityTests(8 cases). - Brian Vogel's #177 review also surfaced two still-parked, pre-existing items — the Reading-Pane reading pane not being resizable, and account deletion triggering repeated re-auth prompts — neither of which is addressed here.
Calendar fixes & polish (#290, #291, #293, #297)
- #290 — switching calendar views (A/D/W/M) now moves keyboard focus into the new view, so the arrow keys work immediately instead of needing a Tab-away-and-back.
CalendarViewModel.SwitchViewraisesListFocusRequested. - #291 — with calendar field labels off (concise mode), event rows carry no field labels at all (comma-separated values only); the appointment editor shows the event's calendar in a read-only, tab-reachable field.
CalendarEvent.DisplayLinedrops theLocation:/Organizer:labels;EventEditorViewModelexposes the calendar label. - #293 — Google events on a secondary calendar can now be deleted/edited: the write path (
GoogleCalendarClientcreate/update/delete) was hardcoded tocalendars/primary/eventsand 404'd on any other calendar; it now targets the event's ownCalendarId(URL-escaped,primaryfallback). Adding an account now triggers an immediate calendar sync (TriggerCalendarSyncSoon) so its sub-calendars appear right away instead of only after the next 15-minute pass or a restart. Also fixes tests writing into the user's realquickmail.log(a[ModuleInitializer]redirects test logging to a temp dir). - #297 — the meeting-invite Accept/Decline card now persists across opens.
ImapMailService.GetMessageDetailCoreAsyncsetCalendarInvite(parsed, in-memory) but notCalendarIcs(the raw text the local store caches), so once the background prefetch cached the row the card vanished on every later cache-served open — a prefetch-vs-open race that looked account-dependent. Now the raw ICS is persisted.
Markdown preview window opened blank (#301)
MarkdownPreviewWindow'sNavigationStartinghandler cancelled every navigation exceptabout:, which cancelled its owndata:text/htmlcontent — how current WebView2 runtimes deliverNavigateToString— leaving a blankabout:blankpage. It now allowsdata:through (asMessageWindowalready does); externalhttp/https/mailtolinks still route to the default browser and the content stays CSP-locked inBuildHtml.
Respond to invitations from the calendar
- Pressing Enter on a pending email invitation in the calendar (new
CalendarEvent.IsPendingInvite=!IsUserCreated && !IsGraph && ResponseStatus == Pending) opens aContextMenu(Accept / Tentative / Decline / — / Open full appointment) instead of jumping to the source email. The menu is anchored to the focused row and, because a programmatically-openedContextMenudoesn't reliably focus its first item, anOpenedhandler focuses "Accept" so a screen reader announces it;Closedreturns focus to the list. Every non-pending row keeps its prior Enter dispatch (edit / open source / drill day). MainViewModel.SendIcsReplywas refactored into a sharedSendIcsReplyForAsync(invite, account, partStat, actionLabel, sourceMessageId, sourceFolder); the reading-pane card and the newRespondToCalendarInviteAsync(evt, partStat, actionLabel)both call it. The calendar path loads the invite's source message (LoadDetailAsync→GetMessageDetailAsyncfallback), replies from the account that received the invite (evt.AccountId— not any default, guarding against the #296 mis-routing), announces the result, and updates the row's response status. Palette commandscalendar.acceptInvite/calendar.tentativeInvite/calendar.declineInvite(no default key) are available when a pending invitation is selected. Tests: reply routes from the receiving account and updates status; empty-source guard;IsPendingInvitepredicate.