The task modal and the plugin API send an all-day due as a bare date
("2026-10-14"). A task's due and start are LocalDateTimes (RFC 8984).
Stalwart accepts the bare date and echoes it back over JMAP, but the
stored iCalendar has no DUE, so CalDAV clients (phones, Thunderbird)
show the task without a date. "2026-10-14T00:00:00" with showWithoutTime
is stored as DUE;VALUE=DATE.
createCalendarTask and updateCalendarTask now complete a bare due or
start to midnight. Every write goes through them, so a task saved by an
older client gets a proper due when it moves to another account.
Tests: create, update and the cross-account move against round trips
recorded on Stalwart v0.16.24 (JMAP set, then CalDAV GET of the stored
iCalendar); times and a cleared due stay as they are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part 2 of 3 of the account archive agreed in #1198. When the admin turns on
"Account List Backup: Passwords" (off by default), saving the list offers to
include the passwords of the accounts signed in here with a plain password.
They are encrypted in the browser together with the list. On restore, those
accounts are signed in to in the background, one by one, without leaving the
active account; a refused password leaves the account waiting for a sign-in by
hand, as before. OAuth and TOTP accounts are saved without a password.
Stalwart's default folder set has no archive-role mailbox, so on a fresh
account Archive did nothing but raise the not-found toast (#578). Both
archive paths now go through ensureArchiveMailbox(), which creates
"Archive" with the archive role in the account the action targets (own,
the selected shared folder's owner, or the pinned account in unified
view), reloads the mailbox list and continues with the store's own
object. createMailbox() takes an optional role for this. A refused
creation or a folder that does not come back still surfaces as an error.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A copy between logins goes through `Email/import`, which sent no
`receivedAt`, so the server stamped the import time and every copied
message landed at the top of the destination folder as today's mail.
`importRawEmail` now takes the original `receivedAt` (the same change
#1151 carries), and the cross-account copy and move pass it.
A send answered without an EmailSubmission threw a fixed English
message, which the composer showed as is in every language. It now
throws `SendUnconfirmedError`, and the composer shows the existing
`send_timeout` text, the same advice as for a send whose answer was
lost: check Sent before sending again.
With several accounts connected, mail could only be moved between
them (drag to another account's folder). There was no way to file a
copy in another account and keep the original where it is.
crossAccountMoveEmails and copyEmailAcrossAccounts take an optional
`{ keepOriginal: true }`. With it the source message is not destroyed,
and the store leaves the list, the open message and the selection as
they are; only the destination's folders are refreshed. Without it
nothing changes.
The message menu gets "Copy to account..." when more than one account
is connected. It lists the writable folders of the other accounts
(their folder lists are fetched when the menu opens) and copies the
selection, or the message the menu was opened on, through the new
copyEmailsToAccount action. That groups the messages by the login and
JMAP account they live in and refuses a copy into the account a
message is already in.
When the response carries no EmailSubmission - a method error, or no
answer for it at all - nothing confirms the message left, yet the send
went on as if it had: the old draft was replaced and the composer
closed. It now fails with "Send confirmation was not received" and
keeps the draft, so the user can check Sent before sending again.
An empty notCreated map is a success some servers spell out; it is no
longer read as a failure.
Drafts were saved without In-Reply-To and References, and a draft
re-opens in compose mode, where the composer has no original message to
rebuild them from. A reply that was saved and re-opened, or sent and
brought back with Undo, therefore went out as a new conversation.
`createDraft` now stores the threading headers, the composer saves them
with every draft of a reply, and re-opening a draft carries them into
the composer for later saves and the send.
Marking read, starring, tagging, emptying a folder and cancelling a
scheduled send only checked that the request came back. A `/set` the
server refused - `notUpdated`, `notDestroyed` or a method-level
`error` - resolved as if it had worked: the list showed the new state,
and a cancel the server rejected looked cancelled while the message
still went out.
These writes now go through one check that throws on any of those, so
the store keeps the old state and shows the server's reason.
`getEmails` and `getSomeEmails` caught every failure and answered with
an empty result. A refresh during a server hiccup therefore emptied the
message list, and a push delta whose follow-up read failed treated every
updated row as gone from the folder while still moving the sync state
past those changes, so the missed mail only came back on a full reload.
Both now throw. A refresh that fails keeps the list on screen, a delta
that cannot be read falls back to the full refresh, and the unified
views already record per-account failures instead of showing nothing.
The state poll puts two method calls per session account, plus
calendars and filters, into one request. From seven accounts on (the
server's default limit is 16 calls) Stalwart refused the whole request,
and the failure was swallowed, so new mail and folder changes stopped
showing up for users with many shared accounts. The poll now goes out
in batches of `maxCallsInRequest`, one after the other.
A network error on a JMAP request was retried once after a second.
Timeouts were already excluded, but a connection that failed after the
request was written (a reset, a proxy dropping the answer) still
replayed the whole batch, so one click on Send could queue two
submissions and deliver the mail twice. Only requests that change
nothing are retried now: GETs, blob uploads and batches made only of
read methods.
A message sent or scheduled from a shared or group identity lives in
that account. Undo send, "Edit" on a scheduled message and cancelling
one cancelled the submission in the right account (#874), but then
restored the message to Drafts, fetched and deleted it through the
login's primary account. That hit whichever primary message had the
same id - pulling an unrelated mail into Drafts or deleting it - and
left the unsent message sitting in the group's Sent.
`restoreEmailToDraft` now takes the account, and the store passes the
submission's account to restore, fetch and delete, using that
account's Drafts and Sent.
Attachment thumbnails were object URLs typed with the sender's MIME
type. Object URLs share the webmail origin, so an `image/svg+xml`
attachment opened from its thumbnail ("Open image in new tab", or
dragged to the tab strip) ran the sender's script as the webmail in
Chromium and Firefox. The drag-out URL and Files thumbnails had the
same shape.
`fetchBlobAsObjectUrl` now re-types every blob to an inert type, and
image thumbnails in mail and Files go through `imageBlobUrl`, which
hands an SVG over as a `data:` URL: it still renders in the `<img>`,
but opened on its own it gets an opaque origin.
The Authentication-Results parser matched `dmarc=` and `dkim=` anywhere
in all headers joined together. A sender who used `dmarc=pass@...` as
the envelope address got a green DMARC pass for a message that failed,
because the server echoes that address in the SPF clause before its
real `dmarc=fail`. A sender could also add their own
Authentication-Results header to an unsigned message and get a DKIM and
DMARC pass. Calendar invites from such a message were shown as trusted.
The parser now splits each header on `;` outside quoted strings and
comments and reads the method only at the start of a part. DKIM, DMARC
and iprev come from the topmost header, which the receiving server
wrote; lower headers can only escalate SPF to a failure.