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.
After a password change the session kept authenticating with the old
password: the JMAP client's Basic header, the server-side context the
Stalwart management calls use, and the remembered login. The next
request was refused and the user was sent back to the login page.
A successful password change now switches all three to the new
password for Basic-auth sessions. OAuth sessions are unaffected by this
change.
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`.
Stalwart refuses a VacationResponse subject of 512 bytes or more and a
text or HTML body of 2048 bytes or more, answering only "Field could
not be set." The limit counts bytes, so 1030 "ä" already exceed it,
and the HTML from the rich-text editor reaches it quickly. The save
failed with a generic error.
On Stalwart the vacation settings now check the subject and the text
and HTML that would be sent, and name the limit instead of saving.
"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.
Bulwark read the live display name (#900) and the account profile with
`x:Account/get`, which Stalwart only allows for admins. Every other
user got `forbidden`, so the settings page showed no name and the
identity kept the stale snapshot. The name now comes from
`x:AccountSettings/get`, the account's own settings object; aliases
and quota still use `x:Account/get` where it is permitted.
With TOTP on, Stalwart requires the current code for a password change
and for switching TOTP off ("Current OTP code is required"). Neither
dialog asked for it. Both now do, and the password change sends it as
the `otpAuth/otpCode` patch, because a whole `otpAuth` object would
reset `otpUrl` and turn TOTP off.
Saving the display name also ignored a `notUpdated` answer and showed
success.
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.
Selecting a tag does not clear an active search: `handleTagSelect` leaves
`searchQuery`/`searchFilters` alone and `selectKeyword` leaves
`selectedMailbox`, so a tag view and a search are active together. The
paths then disagreed about which one wins:
fetchEmails / refreshCurrentMailbox → `selectedKeyword` first, so the
tag fan-out ran and the query was dropped;
searchEmails / advancedSearch / loadMoreEmails → the search first, so
since #1082 the all-folders fan-out ran and the tag was dropped.
Both orders are defensible on their own; having both is not. A
push-triggered refresh silently replaced a search result list with the
whole tag, and "load more" appended rows the tag does not contain — the
same class of inconsistency #1082 set out to remove between a search and
its own next page.
Resolve it the way the codebase already documents ("a tag view takes
precedence over the selected folder"), and keep the query: a search inside
a tag view now narrows the tag, in all five paths. `fetchTagEmails` grows
an optional `extraFilter`, which `getEmails` already accepts alongside
`hasKeyword` — the same argument the message-list category tabs use — so
no new query plumbing is needed.
Searching within the tag is also the more useful reading: a tag view is a
list the user deliberately opened, and a search box above it reads as
"within this", not "forget this".
Remove packages nothing imports any more (`@tiptap/extension-image`,
`asn1js`, `pkijs`, `pvtsutils`, `sonner`, `webcrypto-liner`) and the
`elliptic` override, which only applied through `webcrypto-liner`.
`sharp`, `undici`, `@tiptap/extension-paragraph` and
`@tiptap/extension-heading` were imported directly but only resolved as
transitive dependencies of next, jsdom and starter-kit. List them
explicitly and point the `undici` override at `$undici` so both stay in
step.
`selectAccountMailbox` switches the viewed account but kept
`searchMailboxId`, which names a folder of the account being left.
Since #1082 that scope resolves against `resolveActionMailboxes()`, which
now returns the NEW account's mailbox list, so the id no longer matches
anything. `searchEmails` / `advancedSearch` then take the picked-folder
branch with `mailbox === undefined`, fall back to `jmapMailboxId =
searchMailboxId` and `accountId = undefined`, and query a foreign folder
id against the wrong account — which comes back empty.
It is invisible in the UI: the <select> binds `value={searchMailboxId}`
and has no matching <option>, so a native select renders the first one and
the dropdown reads "All folders" while the store still holds the stale id.
The user sees an empty result list for a search they believe is unscoped.
Reset it on the account switch, so the scope always matches what the
dropdown shows.
Rolling back ran the container from the local tag, so the demo recorded
`bulwark-demo:previous` as its image — and that tag is moved by the very
next deploy. Run the id instead; the tag stays only to keep the image from
being pruned.
The demo container followed ghcr.io/bulwarkmail/webmail:latest through
Watchtower, so a release reached it silently and there was no way to see,
trigger or undo it. It is now a deployment of this repo:
- .github/workflows/deploy-demo.yml runs after "Publish Docker Image on
Release" succeeds, or on demand with a tag
(`gh workflow run deploy-demo.yml -f tag=1.10.0`, or action=rollback).
It only talks to the server; nothing is built.
- deploy/demo-deploy.sh is the forced command behind the deploy key
(/usr/local/bin/bulwark-demo-deploy): pulls the tag, recreates the
container with the demo mounts, checks /api/health and goes back to the
image it replaced if that fails. A pre-release moves no :latest, so the
deploy after one finds the same image and does nothing.
Watchtower has been recreated without bulwark-demo; it still updates the
two rbm.systems containers.
Keep the latest Docker tags on the stable release when a GitHub release is marked as a pre-release, so auto-updating deployments on :latest do not pick up beta builds.