Stalwart 1.0 fails every CalendarEvent get and query whose `timeZone`
argument names a zone missing from its time zone database, with
invalidArguments "Invalid timeZone". 0.16 reads such a zone as UTC, so
the calendar kept working there; on 1.0 it would stay empty.
- check the user's zone with a get of no events that rides along with
the session's first calendar list
- once the server refuses the zone, ask in UTC, move the grid's
wall-clock range bounds to UTC as well, and log one console warning
- repeat a read that was refused before the check answered, once, in
UTC
A sender or subject order is collated by the server. Stalwart 1.0 ranks
byte-order sort keys (lowercased name and address, the thread name for
the subject) and puts empty values last in both directions. The client
re-sorted every page and every thread group with `localeCompare` and
empty values first, which turned that order around.
Pages and thread groups whose order has a sender or subject level now
keep the query's order (the Email/get list is put back into the query's
id order). Other orders keep the client-side safety sort, and the
unified view still merges accounts with the client comparator.
Stalwart 1.0 puts `$hasattachment` or `$hasnoattachment` on every
message and derives `hasAttachment` from them alone. It also refuses
keywords over 128 bytes (0.16 cuts them to 128 characters) and more than
100 keywords on one message. The keyword writers ignored Email/set
`notUpdated`, so a refused tag showed until the next reload and then
vanished.
- `markAsRead`, `batchMarkAsRead`, `toggleStar`, `updateEmailKeywords`,
`setKeyword`, `removeKeyword` and `batchUpdateKeywords` check the
Email/set outcome, and the errors carry the JMAP type. Tagging from
the list or the viewer shows "Tagging failed"; a refused category-tab
move rejects the plugin's promise and skips the categorize hooks.
- Tag ids are capped at 121 characters (128 bytes with `$label:`).
- The attachment keywords are hidden in view source, the plugin
projection and the `getKeywords` fallback labels, reserved for
category tabs, and refused by the plugin keyword API. Email/copy and
the cross-server import still carry them over.
- A tag comes off by leaving its key out of the full keyword map instead
of being sent as false, which RFC 8621 does not allow;
`updateEmailKeywords` also drops the false entries local updates leave
behind (`$seen: false` after marking unread).
- The automatic mark-as-read no longer leaves an unhandled rejection
when the server refuses it.
Stalwart 1.0 advertises `urn:ietf:params:jmap:metadata` by default, but
its handlers are still `todo!()`: a get that selects `metadata` or
`privateMetadata`, a /changes call with `ignoreMetadataOnlyChanges`, or
an Email/query filter such as `metadataExists` panics the request on the
server.
- Mailbox/get and SieveScript/get ask for explicit RFC property lists
(the same as today's 0.16 and 1.0 defaults) instead of none, so a
later default that includes metadata cannot reach them.
- Plugin category tabs (`tabs.set`) reject JMAP Metadata filter
conditions at any depth; the tab query went to Email/query verbatim.
- A test checks the requests the client builds, and the request-building
sources, for metadata properties, the /changes argument and the
capability.
`contactToWire` sends a key that is present but undefined as null, so a
caller passing a card back without `uid`, `id` or `addressBookIds` (the
plugin API does) asked the server to clear them. Stalwart 1.0 refuses a
null UID and 0.16 silently deletes it; a null `id` or `addressBookIds`
fails the update on both. Update mode now leaves those keys out.
vCard export prefixed every group member with `urn:uuid:`, but keys are
already URIs for groups made in Bulwark (members are keyed by uid) and
for groups from CardDAV clients, which gave
`MEMBER:urn:uuid:urn:uuid:…`. Keys with a URI scheme are written as they
are.
Stalwart 1.0 checks names more strictly than its capability says: a
reserved name may not be the stem either (`con.txt`, `Aux.tar.gz`), and
control characters are refused (macOS `Icon\r`). Names over 255 bytes
of UTF-8 were never accepted. Uploaded names are now made acceptable
before they are sent: control characters become "_", a reserved stem
gets "_" after it (`con.txt` becomes `con_.txt`), and a long name is
shortened, keeping its extension. The rename and new-folder dialogs
refuse such names with their own messages. The rules apply wherever the
server publishes naming rules, so 0.16.6 to 0.16.23 get `con_.txt` too.
One refused file used to abort a whole upload. Uploads now go on and
report the files that failed at the end, and a folder upload skips the
contents of a folder it could not create instead of dropping them into
the folder the upload started in. What would sit deeper than 1.0's
`maxFileNodeDepth` (64) is left out before it is uploaded, and a folder
copy that would go past it fails before the first create.
File errors keep their JMAP type, and the Files toasts now say why an
action failed: too many files or folders (`overQuota`), folders nested
too deeply, a refused name, missing rights, or a file too large. New
and copied files get a bare lowercase media type, which 1.0 requires.
Bulwark counted a FileNode as a folder when it had no blobId. Stalwart
1.0 adds symlinks, which have no blob either, so they showed up as
empty folders that could be opened, and folder copies recreated them
as such. Listings now ask for `nodeType` (returned since 0.16.6) and a
single helper, `fileNodeKind()`, decides what a node is in the Files
store, the copy code and the global search, falling back to the blobId
rule when `nodeType` is missing. `nodeType` is never sent in
FileNode/set.
A symlink is shown as an inert row with a link icon. It can be renamed,
moved and deleted, but not opened, previewed, downloaded, copied or
shared, and folder copies skip it.
Stalwart 1.0 lists the folders above a node shared with the user, so
the tree can be walked down to it, but without dates and with every
right false. Bulwark took the owner's top folder for the shared root:
the folder that was actually shared sat several levels further down,
and rename, delete and move were offered on folders where they could
only fail.
Nodes of other accounts with no `modified`, no `created` and no rights
are now marked as placeholders. They are left out of the shared roots,
of shared-drive listings and of search hits; search still uses them to
show the path of a hit below them. 0.16 always returns `modified`, so
nothing changes there.
Copying across accounts went through Blob/copy, which failed with
`unknownMethod` on Stalwart 0.16.10+ and 1.0 because
`urn:ietf:params:jmap:blob` was not in `using`. Even with it, the copy
would have been reserved to the target account, which only its members
may use, so copying into another user's shared drive stayed refused.
Copies now point at the source's blobId: Stalwart resolves a FileNode
blob from another account for anyone who may read that node, in both
directions and on every supported version.
A folder copy lays out the whole subtree before the first create. If a
create fails part-way, the half-made copy is destroyed with its
children and the server's error is reported, instead of leaving part
of the tree behind. Files created over WebDAV inside a copied folder
are no longer copied under their percent-encoded names (#869).
New files are created without `size`: every server takes it from the
blob, and 1.0 refuses a size that differs from the blob's.
Stalwart 1.0 reads a text filter value that starts and ends with a
quote as one exact phrase, and a value ending in `*` as one prefix over
the whole string, not split into words and capped at 16 bytes. Every
search path sent the typed input as one `text` value, so on 1.0
`"annual report" 2024 "draft"` became a single four-word phrase and
`project budg*` a prefix containing a space: both found nothing.
`"Q3 planning" budget` lost its phrase on every version.
`splitSearchText` (`lib/jmap/search-utils.ts`) now sends each quoted
phrase and each `word*` as a condition of its own, ANDed with the rest.
The remaining words stay one condition, so a query without quotes or
stars goes out as before. Outer single quotes are stripped from that
part, since 1.0 reads them as a phrase too. The split applies to:
- the search bar, tag views, cross-account and all-folders search;
- advanced search, including the from/to/subject/body fields;
- contact search;
- global search for mail, contacts and calendar.
`inMailbox` and the calendar bounds sit next to the text conditions in
one AND, with one key per condition: Stalwart rejects an operator that
also carries property keys, and 1.0 allows only one AND group when it
expands recurrences.
SearchSnippet/get switches the whole request to exact matching once a
term is quoted, which marks nothing for a phrase ANDed with other
words, so the snippet filter unquotes phrases.
On 1.0 and on external search backends each phrase is now a real
phrase. The 0.16 built-in index reads it as an AND of its words,
without stemming. The dev mock and the demo client now evaluate AND
filters and quoted phrases, so they keep working with the new shapes.
Stalwart's change ids only grow. An upgrade that keeps the old change
log but restarts the counters lower (1.0 counts per collection where
0.16 counted per account) makes Email/changes and Mailbox/changes
accept an old state and answer with a lower `newState` and no changes,
so the delta sync would silently skip everything that changed since.
Read Stalwart `s` states (LEB128 in Stalwart's base32) and treat a
`newState` below `sinceState` like `cannotCalculateChanges`: fetch in
full. The check runs only on accounts that list `urn:stalwart:jmap`,
only for states in the exact form Stalwart writes, and never trips
while states go up.
Stalwart 1.0 allows 100 Principal/getAvailability calls per account a
minute and counts every call, however they are batched. The event form
sent one request per attendee whenever the time or the attendees
changed, and showed "Available" for an empty answer, which is also what
the server returns when the user may not see that person's free/busy.
- Answers are cached for three minutes per principal and the local
days the event touches, so moving an event within its day asks
nothing new; the status for the exact time is worked out locally.
- Lookups already on their way are shared, and the principals without
an answer go out in one request, split by `maxCallsInRequest`.
- After `rateLimit` nothing is sent for a minute, and attendees without
an answer show "Couldn't check".
- An empty answer shows "No busy times shared" instead of "Available".
0.16 has no rate limit; it gets the same labels with fewer requests.
Stalwart refuses a push subscription's whole emailPush map as
`forbidden` when it names a mailbox shared by ACL (0.16.16 and later,
and 1.0). Bulwark put every account with mailboxes in the map, so
enabling push showed raw JSON, and the background re-sync destroyed the
working subscription after its refresh was refused and dropped its key:
push stopped without a word and could not be turned back on while the
share existed. A network error, a timeout or rate limiting during the
refresh ended it the same way.
- The map holds the primary account and the accounts that list
`urn:ietf:params:jmap:emailpush`, which 1.0 leaves off ACL shares.
0.16 lists it everywhere, so a refused map narrows to the accounts
that look owned, then to the primary account, as the mobile app does.
Refused accounts are remembered under
`bulwark.push.emailPushRefused.v1.<id>`; Re-register and disable
clear the list.
- A subscription ends only when a verified replacement exists, the user
turns push off, or the server no longer has it. The old record is
destroyed after the new one is verified. A failed listing, a network
error or a refused method call leave it and its key alone.
- PushSubscription/set failures throw typed errors (`jmapSetError` for
a refused object, `jmapError` for a refused call) instead of JSON in
the message or `false`, and the client exposes `getAccountInfo`.
- Settings says that mailboxes others share don't send background
notifications, and `forbidden` and `overQuota` read as plain text in
settings and the onboarding prompt.
Stalwart 1.0 refuses Principal/query and Principal/getAvailability with
`forbidden` unless the administrator allows directory queries, which is
off by default. `getPrincipals` turned that into an empty list with no
signal: the share dialog said "No other users or groups found", existing
shares showed bare ids, and free/busy called every attendee "Not a user
on this server".
- `listPrincipals` says whether the listing is complete, `restricted`
or failed. On `forbidden` it falls back to Principal/get without ids,
which 1.0 still answers with the user's own account, their groups and
the accounts that shared something with them.
- Principal/get asks for `id`, `type`, `name`, `description` and
`email` only; 1.0's default list adds `capabilities` and `accounts`.
- One listing per client, kept for five minutes, serves the share
dialog, recipient autocomplete and free/busy, which used to download
the directory three times.
- The share dialog explains a restricted directory while still offering
its candidates, says when the list failed to load, and shows "Unknown
user" with the id for sharees it can't name.
- Free/busy first reads `mayGetAvailability` from the user's own
principal and stays hidden when it is false or a lookup is refused
with `forbidden`. "Not a user on this server" now needs a complete
listing; failures and `rateLimit` show "Couldn't check".
On 0.16 with the default roles the listing is complete and the dialog
behaves as before.
Stalwart 1.0 refuses series properties such as privacy and the
organizer on a single occurrence unless they repeat the series' value,
and refuses them inside a recurrence override on the base event. The
event modal sent privacy "public" on every save, so editing one
occurrence of a series without CLASS failed, and so did every change to
an occurrence the browser expanded.
- leave series and per-event keys out of changes to one occurrence
- write base-event overrides as before and, if the server refuses
series keys there, repeat the write once without them and remember
that
- stop resetting status, free/busy and privacy when an event is edited
Stalwart 1.0 refuses an expanded CalendarEvent/query longer than
maxExpandedQueryDuration (365 days by default) with
expandDurationTooLarge. The scrolling month view reaches 777 days and
search with after:/before: can span years, so the grid went blank.
Range queries are now split into chunks within the advertised limit and
merged. A chunk the server still refuses is halved; if a week still
expands to too many occurrences, the range is expanded in the browser
as before. 0.16 advertises the same limit without enforcing it and
returns the same events.
Stalwart 1.0 returns every photo, logo and sound in a contact card as a
blobId without a uri, including every photo Bulwark saved as a data:
URI. Bulwark read only `uri`, so avatars, the contact list and detail,
print, vCard export and the "has photo" filter showed nothing, and the
form sent `media` without the photo on every save, so editing a phone
number deleted the photo on the server.
- `ContactMedia` carries `blobId`, `label`, `pref` and `contexts`; `uri`
is optional, and `getContactPhotoRef`/`hasContactPhoto` read both.
- Blob photos are downloaded once each, at most two at a time and at a
paced rate, only while on screen, into an LRU of object URLs revoked
on eviction, account switch and logout. They share Stalwart's
per-account budget of 4 concurrent requests and 1000 a minute.
- Avatars look senders up in an index built once per contact list.
- The form leaves `media` out unless the photo changed, and otherwise
passes the other entries through as the server returned them.
- A new photo is uploaded into the user's own account and sent as a
blobId where the blob capability lists ContactCard (1.0). 0.16 refuses
blobIds in media and keeps getting a data: URI; a refusal at runtime
falls back the same way for the rest of the session.
- Print and vCard export download blob media first; duplicate and
cross-account move send the bytes instead of the source's blobIds.
- vCard export also writes photos whose data: URI names no media type
(`data:;base64,`, `data:base64,`), which it dropped before.
Stalwart 1.0 counts every EmailSubmission record of an account against
`maxSubmissions` (500 by default), whatever its status or age, and its
own purge of old records never runs. Bulwark never destroyed a
submission, so after about 500 JMAP sends an account could not send at
all: every send, read receipt and reschedule failed with a raw "There
are too many email submissions" error, and each failed attempt left one
more copy of the message in Drafts.
Bulwark now removes the records that can no longer be cancelled.
Destroying a record never stops its delivery, but a pending one could
no longer be cancelled, so pending records are never touched:
- an immediate send (no hold, no undo window) destroys its own record
right after the send, riding along with the old-draft cleanup; only
where `urn:stalwart:jmap` is advertised, and never decided by the
created `undoStatus`, which 0.16.10-0.16.16 report as final for held
sends
- a throttled background prune destroys canceled records and final
records whose `sendAt` has passed, going by `EmailSubmission/get`, in
every submission account; it runs at startup, after sends, after
undo and on EmailSubmission state changes
- a confirmed cancel (undo, cancel, reschedule) destroys the canceled
record in a request of its own
- an `overQuota` on a submission create prunes at once, on Stalwart
walks the change log for records older than the query's three-day
window, retries once, and otherwise throws `SubmissionQuotaError`,
shown as a translated explanation in the composer, quick reply, read
receipts and the scheduled-send actions
Failed sends now clean up after themselves and say so:
- a failed send removes the Drafts copy it created and keeps the
user's draft; the raw send paths remove their import the same way
- a read receipt that did not go out is taken back out of Sent, a
method-level error counts as a failure, `$MDNSent` is only set after
a sent receipt, and the automatic receipt no longer retries on every
refresh of the open message
- quick-reply failures, "Send now", "Reschedule" and a failed undo are
shown instead of only logged
- the scheduled list reads every page of `EmailSubmission/query`; it
stopped after the first because `total` needs `calculateTotal`
Stalwart 1.0 refuses a CalendarEvent/get that asks for
recurrenceOverrides together with utcStart or utcEnd, and every calendar
read did. The grid, tasks, reminders, search, contact activity and the
agenda plugin came back empty with no error, and creating an event or
task threw after the server had created it, so a retry made a
duplicate.
- read overrides with a second get in the same request, only where
they are needed
- treat an error answer to a get or query as a failure; the calendar
keeps the last events and says it could not load them, with a retry
- never throw after the server created an event or task
- let the server expand series for /api/calendar-agenda
The composer uploaded any file and learned about `maxSizeUpload` (per
blob) or `maxSizeAttachmentsPerEmail` (per message) only from a failed
upload or a failed send. Files over the per-file limit, and files that
would push the message past the per-message limit, are now skipped
with a message naming the limit. The limits come from the account the
message is composed in.
From 0.16.21 Stalwart fails the whole CalendarEvent/set with
`forbidden` when the account may not send scheduling messages, so an
event with attendees was not saved at all and the calendar showed only
"Failed to save event".
Such a refusal now surfaces as SchedulingDeniedError, and the calendar
names the server's reason and offers to save the event without sending
the invitations.
The share dialogs and the availability view listed principals with an
unbounded Principal/query whose ids fed one Principal/get. Once the
directory held more principals than `maxObjectsInGet` (500 on
Stalwart), the server refused the get with `requestTooLarge` and the
list came back empty. The listing now pages in steps of
`maxObjectsInGet`.
"Mark all as read" paged through the unread messages by position, but
each page it marked dropped out of the unread query, so every other
page was skipped: with more unread mail than one page (500), part of it
stayed unread. The next page now starts after the messages that were
left unread (those in excluded folders) only.
Neither mark-as-read loop checked the Email/set result. A refused
update made "mark folder as read" fetch the same unread ids forever;
it now stops with an error. Marking as spam and "not spam" also
ignored `notUpdated` (e.g. a Junk folder the server no longer knows)
and showed success; they now report the failure.
Moving mail into a shared account used `Email/copy` with
`onSuccessDestroyOriginal`. Stalwart before 0.16.15 destroys the wrong
id with it, and from 0.16.15 it destroys the source even when the copy
failed, so a move refused for quota or a missing message deleted the
original with no copy anywhere.
The copy now runs without that flag, and the source is destroyed with
a separate Email/set only after the copy was created. A failed destroy
is reported, leaving a duplicate rather than a loss.
From 0.16.6 Stalwart refuses FileNode names with `/<>:"\|?*` or a
reserved name such as `CON` ("Name contains a forbidden character."),
and publishes the rules as `forbiddenNameChars` / `forbiddenNodeNames`.
Bulwark did not read them: the error showed up only after an upload had
started, and it aborted the rest of a multi-file or folder upload.
- new folder, new text file and rename now say which characters or
names the server does not allow, before sending anything
- uploads store an accepted variant instead: forbidden characters
become `_` and a reserved name gets `_` appended
MIME types over 30 characters (most Office formats) were always stored
as `application/octet-stream`, because Stalwart before 0.16.6 refuses
them. From 0.16.6 the limit is 255, so the real type is kept there.
Stalwart before 0.16.6 implements an older JMAP File Storage draft
whose rights are only `mayRead`, `mayWrite` and `mayShare`. Bulwark
always sent the newer `mayAddChildren` / `mayRename` / `mayDelete` /
`mayModifyContent`, so sharing a file or folder failed there with
`Invalid permission "mayAddChildren"`, and the rights such a server
reported did not match what the UI checks.
When the account's filenode capability lacks `forbiddenNameChars`
(added by the same draft update), shares are now written with
`mayWrite` set from the finer rights, and `mayWrite` read from the
server is spread over them.
Copying a folder sent one FileNode with no content, which Stalwart
0.16.0 rejected ("size") and 0.16.6+ turned into an empty folder. Copy
now recreates the subtree level by level, and copies into another
account move the file contents with `Blob/copy`.
Every write (new folder, upload, rename, move, delete, copy, share)
went to the primary account, including for nodes of a shared drive,
which the listing names "<accountId>:<nodeId>". Inside a shared drive
the server read those ids as local ones and failed or touched the
wrong node. Writes now go to the account that owns the node, with the
bare id, and a move between accounts is refused instead of being sent.
Creates pass `onExists: "rename"`, so an upload or new folder that
clashes with an existing name becomes "name (2)" instead of failing.
Stalwart before 0.16.6 ignores that argument and refuses the clash, so
the client retries with a numbered name there.
CalendarEvent/query reads `after`/`before` as local date-times in the
request's `timeZone`, and Stalwart silently drops a `Z` or offset. The
proactive alert fetch and the contact activity panel sent UTC strings,
so their windows were shifted by the user's UTC offset (two hours in
Berlin in summer). UTC and offset bounds are now converted to the wall
clock of the request time zone before the query is sent.
With server-side expansion, a range with more than
`maxRecurrenceExpansions` occurrences (a daily series over ten years)
failed the whole query with `invalidArguments`, and the calendar showed
nothing. The query now falls back to the unexpanded series, which the
calendar expands on the client as it does for older servers.
The query also stopped at a fixed 1000 ids. Without an explicit limit it
now pages until the range is complete.
Stalwart rejects a whole ContactCard over one property it does not
know, and several reached it:
- the form sent `calendarUri`, `schedulingUri` and `freeBusyUri`,
so any contact with a calendar, scheduling or free/busy link failed
to save ("Invalid property.")
- vCard import sent `source`, and Pro mode left `localAccountId` on
the card
- vCard import wrote addresses as flat `street`/`locality` fields
instead of RFC 9553 `components`
The card now passes through `lib/jmap/contact-wire.ts` on its way to
the server: client-only fields are dropped, the links are written as
`calendars` / `schedulingAddresses` (and SOURCE as a `directories`
entry), flat addresses become components, and reads map the links back
for the form and detail view.
Clearing a field (nickname, notes, a link) never reached the server:
the form set it to `undefined`, which JSON drops, and an omitted
property keeps its value. Updates now send `null` for cleared keys.
Deleting an address book that still held contacts always failed with
`addressBookHasContents`. The delete now passes
`onDestroyRemoveContents`, which the confirm dialog already promised.
Stalwart advertises `maxDelayedSend` as a fixed 30 days, but its MTA
rejects any hold beyond `futureRelease`, 7 days by default. Scheduling
a message 8 to 30 days ahead passed the picker and then failed with
"501 5.5.4 Requested hold time exceeds maximum of 604800 seconds".
On Stalwart the advertised 30 days now counts as 7. When a submission
is still rejected for its hold time, the limit from the error message
replaces it for the session, and the composer shows the translated
"later than the server allows" message instead of the raw SMTP reply.
JMAP names a failed method call `"error"` (RFC 8620 §3.6.2). The send
paths (normal send, raw/S-MIME send and submit, iMIP reply, invitation
and cancellation) and Sieve validation tested for `"<Method>/error"`,
which never matches, so a refused Email/set or EmailSubmission/set
could be reported as sent.
A method error before the submission is created now fails the send.
One after it comes from the implicit onSuccessUpdateEmail /
onSuccessDestroyEmail call: the message has already left, so it is
reported as a filing warning, not as a failed send that would invite a
duplicate resend.
Search turned every failure, including a method error such as
`unsupportedFilter`, into an empty result that read as "No results
found". A failed Email/query now throws, which the search view shows as
an error and the "All folders" fan-out reports per account.
Stalwart only moves spam to Junk when nothing else decided where the
message goes. Any Bulwark rule that moved or copied mail to a folder
therefore delivered spam there too, e.g. a "subject contains invoice"
rule collected phishing. From 0.16.22 this also applies to the "Keep"
allow-list action, which older versions still send to Junk.
Move and copy rules now add
`not spamtest :percent :value "ge" :comparator "i;ascii-numeric" "50"`
to their condition when the server supports `spamtestplus` and
`relational`. 50 % is Stalwart's spam threshold on the graded scale
(0.16.19+), and older versions report 0 or 100. A new per-rule option
"Also move messages marked as spam" drops the guard, and "Keep" rules
are left unguarded as the explicit allow-list.
The Sieve generator wrote actions in the order they were picked in the
rule editor. `fileinto` stores the flags set at that moment, so "mark
read", "star" or "add label" placed after a move never reached the
moved message. Flag actions are now emitted first.
Moves and copies also targeted the folder by path only. Renaming the
folder silently sent the rule's mail to Inbox. Rules now store the
folder id and emit `fileinto :mailboxid "<id>" "<path>"` when the
server lists `mailboxid` (declaring `mailbox` too, which Stalwart
requires), with the path as the fallback. Saving a rule refreshes the
path from the id, so existing rules pick up the id on their next save.
Forward actions get a "Keep a copy" option (`redirect :copy`), and the
editor honours the server's `maxNumberRedirects` (1 on Stalwart):
forward is disabled once the limit is used up, with a hint when
existing rules already exceed it, because the server skips extra
redirects without telling anyone.
Stalwart allows one active Sieve script and turns the auto-reply on by
activating its own `vacation` script. Turning the auto-reply on switched
every Bulwark filter off (including junk and folder rules), and saving
any filter switched the auto-reply off again.
When the vacation script takes over from an existing filters script,
the filters script is now re-activated with
`include :personal :optional "vacation"`, so the server-built reply
(with its date window and HTML body) runs from there. Turning the
auto-reply off drops the include again. A filters save while the
vacation script is active keeps it through the same include, and the
vacation settings read the include as "on", because
VacationResponse.isEnabled reports false in that state.
Clients that only look at VacationResponse (Stalwart's web UI, phones)
still show the auto-reply as off while the include is active.
JMAP thread ids are only unique within their account - Stalwart hands out
per-account counters such as "b" - so the cross-account views merged
unrelated conversations that happened to share an id, and every
client-side thread map (expansion, cache, loading flag, counts,
virtualizer key) mixed them up the same way.
Give ThreadGroup an account-scoped `threadKey` derived from the email's
source stamps and key grouping and all thread state by it. `threadId`
stays the raw id for Thread/get, which #281 already routes to the owning
account. fetchThreadEmailCounts now asks each source account for its own
threads instead of one client for every id. Single-account views carry no
stamps, so their key is the bare id and nothing changes there.
CalDAV-originated tasks can retain uppercase, underscore-separated, or single-l status spellings in CalendarEvent responses. Normalize known spellings at JMAP response boundaries so task filters and completion controls see canonical JSCalendar values while unknown future values remain intact.
Work around the upstream conversion defect tracked at https://github.com/stalwartlabs/calcard/issues/20.