`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 treats a FileNode's `modified` as the client's: a content
update leaves it alone unless the patch asks for it. Saves from the
document editor therefore kept the old date in the file list, and the
editor's conflict check (409 with COOLStatusCode 1010) never noticed a
change made by another JMAP client. PutFile now sends `modified: null`,
which has the server stamp the save. 1.0 echoes the new time in
`updated`; 0.16.6+ answer `null` as before and the batched FileNode/get
reads it.
The editor also could not open a file in someone else's shared drive,
on any version: the Files app sent the listing's "<owner>:<id>" id for
the user's own account, and the launch failed with a 404. It now names
the owner's account and the node's own id, so owner and sharee join
the same editor session. A save uploads into the user's own account
and then points the owner's node at that blob, because Stalwart only
lets members of an account use a blob uploaded into it. The access
token carries that upload account; tokens minted before it upload into
the document's account as before.
The editor now also uses the cookie slot of the login the Pro shell is
browsing, not always the active one.
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 returns recurrence overrides as RFC 8984 PatchObjects, with
keys like participants/p1/participationStatus and nulls that remove.
Browser expansion spread them onto the occurrence, so a declined
occurrence stayed accepted and the pointer keys became junk properties.
Moved or resized occurrences also kept the time and length the rule
gave them and were placed by their original date.
Overrides are now applied as patches, occurrence times come from the
patched start, time zone and duration, and new changes merge into an
existing override without keys that are prefixes of one another.
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.
Bulwark asks for a 90-day push subscription, but Stalwart clamps
`expires` to 7 days. The background renewal ran once per page load,
so a tab or installed app left open for more than a week let the
subscription lapse and notifications stopped without a hint.
The renewal now runs at most once a day instead of once per load, and
also when the tab comes back to the foreground.
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.