Released July 20, 2026 — downloads for this release
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.
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:
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.
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.
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.
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.
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:
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.
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 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.
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:
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.
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 —
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.
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, 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.
Found a problem or have a suggestion? There are three ways to reach us — pick the one that fits:
Full details, including exactly what a report contains (and what it never contains), are in the Reporting Issues section of the User Guide.
docs/planning/full-calendar-pm-dev-spec.md)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 by CalendarViewModel.ViewMode +
ReferenceDate. Month is a 42-cell grid with arrow
navigation and Enter-to-drill.RecurrenceRule, RecurrenceExpander):
FREQ daily/weekly/monthly/yearly, INTERVAL,
weekly BYDAY, and one COUNT/UNTIL
end condition. Expansion is local wall-clock (DST-safe) with a ~10-year
iteration cap. Per-occurrence edit/delete uses EXDATE +
detach; whole-series edits preserve EXDATEs and the
original start.is_all_day column; re-anchored to local midnight across
all providers to avoid off-by-one-day display). The “timed events only /
deferred” comment in EventEditorViewModel is stale and
should be removed.Prefer: outlook.timezone for Graph, RFC3339 offsets for
Google, kind-carrying stamps for CalDAV).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 with
NotSupportedException); 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.Calendar sentinel node with
All Calendars / Local Calendar / one child per
opted-in account, and — when an account has more than one calendar — a
grandchild per calendar. CalendarFilterFor maps a node to
CalendarViewModel.SourceFilter (a
CalendarFilter(Guid? Account, string? CalendarId)
record).LocalCacheCalendarProvider harvests
text/calendar parts; Accept/Tentative/Decline event card in
the reading pane sends an ICS REPLY and upserts the response status
immediately; METHOD:CANCEL marks events cancelled (filtered
from all views).CalendarReminders /
CalendarReminderMinutes, default off / 10 min), Windows
notification + AnnouncementCategory.Result, fired at most
once per (uid, start) per run.IcsModel.ExportEvent
(calendar.exportEvent, no default key).ShowDeclinedEvents remains config-only (read at
construction; not live-updated).AccountModel.SyncCalendar
bool (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 a
Click handler). Offered for Microsoft, Google, and iCloud —
a superset of contacts.GraphCalendarSyncService.SyncAllAsync now gates on
SyncCalendar; new public
SyncAccountCalendarAsync /
RemoveAccountCalendarAsync drive the enable/disable paths.
IsCalendarPushAccount (save-target picker) and the calendar
tree loop also gate on the flag.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.RequestCalendarConsentAsync
(Calendars.ReadWrite) at opt-in; Google’s calendar scope is
already granted at mail sign-in; iCloud needs none. The
OAuthRouter routes only Microsoft to the consent call.SettingsViewModel CalDav
members, [caldav] config keys, and the synthetic-id
(AccountIdFor / SecretKeyFor) helpers — all
superseded by the per-account model.calendar.syncNow (“Sync Calendars
Now”). Tests: per-account CalDAV sync (rewritten
CalDavCalendarSyncTests), opt-in gating,
ShowCalendarSyncOption,
IsCalendarPushAccount.CalendarEvent gains
CalendarId/CalendarName (two idempotent
ALTER TABLE migrations, no schema-version bump); every
event is tagged with its source calendar.ReplaceGraphCalendarEventsAsync(account.Id, union)):
Microsoft GET /me/calendars → per-calendar
calendarView; Google calendarList →
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.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.CalDavCalendarClient gains
PutEventAsync/DeleteEventAsync (the
redirect/Basic-auth SendAsync refactored into a shared
SendCoreAsync so reads and writes share it); PUT sends
text/calendar with If-None-Match: * on create;
the VCALENDAR body omits METHOD per RFC 4791.
GraphCalendarSyncService create/edit/delete gain an iCloud
branch; IsCalendarPushAccount includes iCloud.CalendarEvent.ResourceUrl, new resource_url
column), 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).https://contacts.icloud.com) using the account’s own
app-specific password (GetPassword(account.Id)), no
OAuth.CardDavContactClient (mirrors
CalDavCalendarClient: manual-redirect Basic-auth
SendAsync, current-user-principal →
addressbook-home-set → collections discovery,
addressbook-query REPORT) + minimal vCard parsing
(VCardModel, reusing IcsModel’s
unfolding/block-scan/unescape).
ICloudContactSource : IProviderContactSource
(ContactSource.ICloud) plugs into
ContactSyncService via an iCloud host guard in
SourceFor. ShowContactSyncOption /
CanSyncContacts extended 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).calendar.goToDate (default Ctrl+G,
rebindable, in the Command Palette) opens a modeless
GoToDateWindow (DatePicker). Day/Week/Month keep their view
and recenter; Agenda switches to Day. RequestGoToDate
announces unavailability in online mode; the picker is
single-instance.ConfigModel.MailSyncPollMinutes (default 5; 0 disables;
clamped 1–120) drives StartFallbackSyncAsync in
MainViewModel, 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.\Seen for
already-cached messages on folder sync, so a message read in another
client stops showing unread.fresh > 0), so users who enable logging via Settings →
Advanced (Log, not Debug) can capture it; the frequent
fresh == 0 evaluations stay Debug-only.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.MessageOpenMode.Tab, the right-pane content region
left the message-list container visible (it was the
DockPanel fill child) while the reading pane showed as a
MinHeight=200 Dock=Bottom sliver, so a message
tab rendered the list plus a body strip rather than the message alone.
Window mode was unaffected (separate MessageWindow);
Reading-Pane mode was unaffected.Grid whose row
sizes swap on a new IsMessageListAreaVisible VM flag via
BoolToGridLengthConverter. The flag is
!(MessageOpenMode == Tab && ActiveTab is MessageTabViewModel && IsMessageOpen)
— the IsMessageOpen term 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).
CycleFocusAsync gates the message-list F6 stop on the same
flag; the window-level Escape handler routes to
ActivateMessageListTab() (revealing the list, leaving the
tab open) in Tab mode instead of CloseReadingPane.TabModeMessageListVisibilityTests (8 cases).CalendarViewModel.SwitchView raises
ListFocusRequested.CalendarEvent.DisplayLine drops the
Location:/Organizer: labels;
EventEditorViewModel exposes the calendar label.GoogleCalendarClient create/update/delete) was
hardcoded to calendars/primary/events and 404’d on any
other calendar; it now targets the event’s own CalendarId
(URL-escaped, primary fallback). 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 real quickmail.log (a
[ModuleInitializer] redirects test logging to a temp
dir).ImapMailService.GetMessageDetailCoreAsync set
CalendarInvite (parsed, in-memory) but not
CalendarIcs (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.MarkdownPreviewWindow’s NavigationStarting
handler cancelled every navigation except about:, which
cancelled its own data:text/html content — how current
WebView2 runtimes deliver NavigateToString — leaving a
blank about:blank page. It now allows data:
through (as MessageWindow already does); external
http/https/mailto links still
route to the default browser and the content stays CSP-locked in
BuildHtml.CalendarEvent.IsPendingInvite =
!IsUserCreated && !IsGraph && ResponseStatus == Pending)
opens a ContextMenu (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-opened
ContextMenu doesn’t reliably focus its first item, an
Opened handler focuses “Accept” so a screen reader
announces it; Closed returns focus to the list. Every
non-pending row keeps its prior Enter dispatch (edit / open source /
drill day).MainViewModel.SendIcsReply was refactored into a shared
SendIcsReplyForAsync(invite, account, partStat, actionLabel, sourceMessageId, sourceFolder);
the reading-pane card and the new
RespondToCalendarInviteAsync(evt, partStat, actionLabel)
both call it. The calendar path loads the invite’s source message
(LoadDetailAsync → GetMessageDetailAsync
fallback), 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 commands
calendar.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;
IsPendingInvite predicate.