Commit Graph
801 Commits
Author SHA1 Message Date
Linus Rath 19c2dfb2b4 fix: keep the calendar working on Stalwart 1.0 with an unknown time zone
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
2026-09-25 19:35:08 +02:00
Linus Rath 9f6b546ffe fix: stop reporting a proxy's version as the Stalwart version in telemetry
The heartbeat's `stalwart_version` came from the `Server` header of
`/.well-known/jmap`. Stalwart never sends one (0.16.0 through 1.0), so
the field was null, or the version of a reverse proxy in front of
Stalwart. The probe is gone: the version is reported only when
`STALWART_VERSION` is set.
2026-09-25 19:32:35 +02:00
Linus Rath 5b0988cb54 fix: send from your own address by default when Stalwart 1.0 lists a group's first
For members of a group, Stalwart 1.0 lists the group's identity (for
example `team@example.org`) before the user's own. The identity sort
put the identity matching the login first, but compared addresses
case-sensitively, so a login typed as `UserA@Example.org` defaulted to
the group address. The identity manager kept its own copy of the same
sort.

Both now use one sort that compares addresses regardless of case, and
with an internationalized domain in either its Unicode or its `xn--`
form.
2026-09-25 19:30:58 +02:00
Linus Rath 4f5e667a53 fix: keep filter scripts valid when a rule contains */ or a bad header name
The rule metadata is JSON inside a Sieve bracket comment, so a `*/` in a
rule name, a value or the vacation text ended the comment early and
broke the script. A line break in a rule name escaped the `# Rule:`
comment line. And a header name outside `[A-Za-z0-9_-]` made Stalwart
(0.16 and 1.0) refuse the whole script, every other rule with it.

- `*/` in the metadata JSON is written as `*\/`, which `JSON.parse`
  reads back as `/`, so older versions still parse it.
- Line breaks in a rule name are flattened on the `# Rule:` line, and
  the parser matches its own blocks the same way.
- The rule editor refuses to save an invalid header name, with a
  translated message. The generator skips such a rule (it stays in the
  metadata, so it can be fixed) instead of writing a script the server
  refuses.
2026-09-25 19:29:30 +02:00
Linus Rath 44c77b268d fix: keep the server's order for sender and subject sorted lists
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.
2026-09-25 19:27:24 +02:00
Linus Rath d7004b3851 fix: report refused tags and keep Stalwart 1.0's attachment keywords intact
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.
2026-09-25 19:24:30 +02:00
Linus Rath a9a25b9403 docs: correct the Lite bundle's update, pinning and CORS instructions
- The Stalwart bundle's README said a failed download keeps the
  previous bundle online. That holds on 0.16.23 only: older 0.16
  releases and the 1.0 pre-releases unmount the Application until a
  later `UpdateApps` succeeds, and on every version a restart after the
  cached copy expired, with `resourceUrl` unreachable, leaves it
  unmounted. The README now says so and tells admins to check the URL
  first.
- The pin URL used a `v` prefix that release tags don't have (404).
- The static README told admins to set `[http] permissive-cors`, a TOML
  key Stalwart 0.16 and later don't read. It now names the
  `Http.usePermissiveCors` setting, with the web admin path, the
  `stalwart-cli` commands and the JMAP call.
- The stated requirement is Stalwart 0.16.6 (0.16.19 for
  `oauthClientId`).
2026-09-25 19:20:41 +02:00
Linus Rath 239e6563f4 fix: keep requests away from Stalwart 1.0's unfinished JMAP Metadata
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.
2026-09-25 19:17:27 +02:00
Linus Rath 606771fee6 fix: keep contact UIDs on update and export MEMBER URIs unchanged
`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.
2026-09-25 18:40:22 +02:00
Linus Rath e712454ec2 fix: store awkward file names and explain file errors on Stalwart 1.0
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.
2026-09-25 18:37:41 +02:00
Linus Rath 92de8e99bd fix: show Stalwart 1.0 symlinks as links instead of empty folders
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.
2026-09-25 18:35:47 +02:00
Linus Rath a7c2e531c7 fix: show shared folders under "Shared with me", not the owner's parents
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.
2026-09-25 18:34:21 +02:00
Linus Rath 9960312b74 fix: copy files between your drive and shared drives
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.
2026-09-25 18:32:43 +02:00
Linus Rath 1cf9944b53 fix: keep the date of edited documents and open them in shared drives
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.
2026-09-25 18:30:08 +02:00
Linus Rath 5967a1c489 fix: find results when a search mixes quoted phrases with other words
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.
2026-09-25 18:06:49 +02:00
Linus Rath 6dc9d5a584 fix: refetch mail when the server's change state goes backwards
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.
2026-09-25 18:04:26 +02:00
Linus Rath f8e42a4a24 fix: cache and batch free/busy lookups under Stalwart 1.0's rate limit
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.
2026-09-25 18:02:55 +02:00
Linus Rath ffa909600c fix: keep web push on when someone shares a mailbox with you
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.
2026-09-25 17:59:17 +02:00
Linus Rath 6152e45b88 fix: explain sharing and free/busy when Stalwart 1.0 keeps its directory closed
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.
2026-09-25 17:45:22 +02:00
Linus Rath d0b0383660 fix: apply recurrence overrides as patches in browser expansion
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.
2026-09-25 17:42:59 +02:00
Linus Rath 990c5e0f77 fix: edit or answer one occurrence of a series on Stalwart 1.0
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
2026-09-25 17:40:49 +02:00
Linus Rath c236d828c1 fix: keep the calendar filled when scrolling past a year on Stalwart 1.0
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.
2026-09-25 17:38:49 +02:00
Linus Rath c23e306789 fix: show and keep contact photos on Stalwart 1.0
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.
2026-09-25 17:35:56 +02:00
Linus Rath 483821706d fix: keep sending working past Stalwart 1.0's submission limit
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`
2026-09-25 17:33:31 +02:00
Linus Rath 8db65135c6 fix: load calendar events and tasks on Stalwart 1.0
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
2026-09-25 17:30:42 +02:00
Linus Rath e147835f0c refactor: carry the JMAP error type on thrown errors through one helper 2026-09-25 16:04:12 +02:00
Linus Rath 3823d2da17 fix: sign in and show addresses on internationalized domains #1100 2026-09-24 21:15:22 +02:00
Linus Rath e33ab899b0 fix: leave out the id token when sign-in cookies would overflow nginx's header buffer #1096 2026-09-24 18:23:11 +02:00
Linus Rath 4d11cbf60b fix: keep the inbox rendering when a message has an unparsable date #1099 2026-09-24 17:47:33 +02:00
Linus Rath 84a639f5d4 fix: keep bringing up unread mail past the first page in unread-first order 2026-09-23 21:01:48 +02:00
Linus Rath f97218375e fix: end the identity provider session on sign-out #905 2026-09-23 20:44:46 +02:00
Linus Rath 1c00cf75cb fix: use the from override as envelope sender where the server allows it #1009 2026-09-23 19:58:26 +02:00
Linus Rath 198c6d41e0 fix: wrap text in fixed-width tables to the screen on iOS #1020 2026-09-23 19:41:44 +02:00
Linus Rath f8592bc6b7 fix: accept oauth endpoints on the configured issuer's own host #1028 2026-09-23 19:26:07 +02:00
Linus Rath a4bc303f45 fix: stop office attachment launch hanging while checking the blob #1047 2026-09-23 19:03:37 +02:00
Linus Rath 7e5b996ee6 feat: open office attachments in mail read-only in the document editor #1047 2026-09-23 19:01:47 +02:00
Linus Rath dc9d609c43 fix: re-authenticate against the account's own server when linking the mobile app 2026-09-23 18:35:15 +02:00
Linus Rath 691cde6468 fix: explain why calendar subscriptions fail with not authenticated #1073 2026-09-23 18:31:27 +02:00
Linus Rath c96814db4c fix: hide the lite server field once config.json sets jmapServerUrl #1087 2026-09-23 18:19:26 +02:00
Linus Rath a69d8a71e9 fix: load attachment chips lazily instead of in the message list #1089 2026-09-23 18:09:35 +02:00
Linus Rath 28fd880582 fix: keep single-occurrence edits on that occurrence and keep its details 2026-09-23 13:40:01 +02:00
Linus Rath f95b826434 feat: let attendees answer a single occurrence of a recurring event #1086 2026-09-23 13:06:41 +02:00
Linus Rath 8c746fb7af fix: keep inbox-only push working for accounts shared without an inbox #983 2026-09-23 05:56:28 +02:00
Aaron Guise 405f6b78db fix(web-push): keep filter entry for junk-less accounts, surface fetch errors in inbox-only 🐛 2026-09-23 11:07:58 +12:00
Aaron Guise 887b9e7f92 feat(notifications): add toggleable inbox-only push notifications ✨ 2026-09-23 11:07:58 +12:00
Linus Rath f086d21d84 docs: use framed standard-theme screenshots in the readme 2026-09-22 23:04:27 +02:00
Linus Rath 8f285e57d3 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-09-22 21:40:54 +02:00
Linus Rath bea0d00053 fix: refuse attachments over the server's size limits before uploading
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.
2026-09-22 21:32:04 +02:00
Linus Rath d7fdd7ce61 fix: offer to save an event when the server refuses its invitations
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.
2026-09-22 21:25:20 +02:00
Linus Rath b217dba461 fix: renew push subscriptions before Stalwart's 7-day expiry
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.
2026-09-22 21:17:48 +02:00