Commit Graph
257 Commits
Author SHA1 Message Date
Linus Rath c3225b3fa8 fix: share files on Stalwart versions before 0.16.6
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.
2026-09-22 20:49:44 +02:00
Linus Rath 1ecddf3cbb fix: copy whole folders and write to shared drives in Files
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.
2026-09-22 20:47:41 +02:00
Linus Rath a002b09e02 fix: query calendar ranges in the right time zone and past the limits
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.
2026-09-22 20:42:07 +02:00
Linus Rath 73d971dbf9 fix: save contacts with calendar links and cleared fields correctly
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.
2026-09-22 20:37:16 +02:00
Linus Rath bab19898d9 fix: cap scheduled send at the 7 days Stalwart actually accepts
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.
2026-09-22 20:31:53 +02:00
Linus Rath 06788567a1 fix: report failed JMAP calls in send and search instead of succeeding
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.
2026-09-22 20:27:16 +02:00
Linus Rath 6241ed61d6 fix: stop folder rules from pulling spam out of Junk
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.
2026-09-22 20:14:42 +02:00
Linus Rath dadfb08f9c fix: keep filter flags and folder targets when mail is moved
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.
2026-09-22 20:09:19 +02:00
Linus Rath 198a3c0d61 fix: keep mail filters running while the auto-reply is on
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.
2026-09-22 20:02:06 +02:00
Linus Rath 9893b0a8ad fix: list every file when the account exceeds maxObjectsInGet #1069 2026-09-22 00:17:44 +02:00
lucamzanon 5b63993bfe fix: scope thread identity to the owning account in unified views #1012
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.
2026-09-18 18:33:02 +02:00
Linus Rath fedfc89d44 fix: list tagged mail from every account in the tag view #1038 2026-09-17 22:26:52 +02:00
Linus Rath 863354d5f1 Merge pull request #980 from sanitz/fix/task-toggle-progress-updated-958
fix: omit progressUpdated from task completion payload (#958)
2026-09-17 22:03:43 +02:00
Linus Rath 15308f59e2 Merge pull request #994 from mulatta/fix/calendar-task-progress-normalization
fix: normalize calendar task progress states
2026-09-17 22:03:38 +02:00
Linus Rath d6c923f20e Merge pull request #947 from shukiv/feat/list-attachment-chips
feat(list): show attachments on list rows and open them from there
2026-09-11 17:05:08 +02:00
mulatta 5f1d279553 fix: normalize calendar task progress states
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.
2026-09-08 23:19:24 +09:00
Linus Rath d28d8dcd86 fix: report calendar clear failures instead of counting zero #434 2026-09-07 22:34:47 +02:00
Linus Rath 4afc2a5db0 fix: surface Email/set failures on delete and move #956 2026-09-07 22:34:47 +02:00
Linus Rath 40e38d1679 fix: retry the JMAP session fetch when a redirect drops the auth header #892 2026-09-07 22:34:47 +02:00
Linus Rath 2b186910d5 fix: subscribe newly created mailboxes #951 2026-09-07 22:34:47 +02:00
Sebastian Sanitz 6b30e91c9f fix: omit progressUpdated from task completion payload (#958)
Stalwart Mail Server strictly validates JSCalendar task properties on CalendarEvent/set and rejects updates containing top-level progressUpdated with invalidProperties (RFC 8984 §5.2.2 only defined progressUpdated on Participant objects, obsoleted in JSCalendar 2.0).

- Omit progressUpdated from toggleTaskComplete in stores/task-store.ts
- Remove progressUpdated from CALENDAR_TASK_PROPERTIES in lib/jmap/client.ts
- Defensively strip progressUpdated in updateCalendarEvent and createCalendarTask
- Make progressUpdated optional in CalendarTask interface
- Add unit tests for task-store and JMAP client task methods
2026-09-06 17:47:00 +02:00
Stefan Hildebrandt 2cf7427e32 fix(email): refetch body value when JMAP truncates the displayed part
Large HTML emails (e.g. image-only reports with a data:image src cut
mid-tag by the 256 KB body cap) rendered completely blank because
Email.bodyValues[partId].isTruncated was declared but never acted on.
On truncation, issue one follow-up Email/get with a much larger cap
for the parts actually shown, batched across a thread instead of one
refetch per message.

Closes #884
2026-09-02 08:34:31 +02:00
Shuki Vaknin 0e1c34cdec feat(list): show attachments on list rows and open them from there
A message with attachments showed a paperclip and nothing else, so the
only way to reach a file was to open the message, scroll to the bottom
and find it. The list rows never had the information to do better:
EMAIL_LIST_PROPERTIES asked for hasAttachment, a boolean.

Request attachments alongside it and render the files as chips under the
preview, each one opening that file directly. Chips honour the existing
mailAttachmentAction setting, so they preview or download exactly as the
viewer's attachment list does.

Inline parts are filtered out. A single Outlook message routinely carries
half a dozen cid: signature spacers of under 200 bytes; chipping those
would bury the one file the reader actually wants. Only parts that are
neither inline nor cid-referenced, and that carry a filename, are shown —
two at a time, with the remainder collapsed into a count.

Downloads resolve the blob source from the row's own message rather than
the selected one, so they stay correct in the unified inbox where each
row can belong to a different account.
2026-09-01 18:51:51 +03:00
Linus Rath 58ebf8d4c5 fix: paginate calendar event fetch so import UID dedup sees all events 2026-08-31 22:48:00 +02:00
Linus Rath b11cf4b1a5 feat: offer deleting a non-empty folder with its messages 2026-08-29 18:23:09 +02:00
Linus Rath 03b588096d feat: filter advanced search by message size 2026-08-29 18:20:21 +02:00
Linus Rath cc90903f05 feat: request delivery notifications and REQUIRETLS when sending 2026-08-29 18:16:52 +02:00
Linus Rath e54d179f32 feat: surface CalendarEventNotification invitations and updates 2026-08-29 18:11:50 +02:00
Linus Rath 14cdb2f6ff feat: organize invitations as the default ParticipantIdentity 2026-08-29 18:07:47 +02:00
Linus Rath e8c594001d feat: show attendee free/busy via Principal/getAvailability 2026-08-29 18:03:26 +02:00
Linus Rath 0b39e43a4d feat: surface ShareNotification changes as toasts 2026-08-29 17:59:04 +02:00
Linus Rath 6e5aca60a9 feat: share mail folders with other users via mail:share 2026-08-29 17:53:54 +02:00
Linus Rath b408b2597a feat: highlight search hits with SearchSnippet/get 2026-08-29 17:47:23 +02:00
Linus Rath 5805a28e8c feat: resolve pushes with Email/changes and Mailbox/changes deltas 2026-08-29 17:42:21 +02:00
Linus Rath 1807b2df6b feat: allow setting an address book as default (#924) 2026-08-29 17:15:50 +02:00
Linus Rath d800689924 feat: use Stalwart synthetic ids for recurring occurrences #140 2026-08-29 11:03:48 +02:00
Linus Rath 63fa2d2d93 fix: stop notifying for spam by using the JMAP emailPush delivery filter 2026-08-26 21:45:12 +02:00
Stefan Hildebrandt ced81835ef fix(calendar): drop default SKIP=OMIT from recurrence rules (DAVx5 sync)
Recurring events defaulted skip:"omit"+rscale:"gregorian" on every rule,
which Stalwart serialises into the CalDAV RRULE as RSCALE=GREGORIAN;SKIP=OMIT.
DAVx5 rejects that ("Invalid recurrence rule"), breaking Android sync. Those
RFC 7529 fields only matter for non-Gregorian scales / invalid-date handling,
so stop emitting them for plain Gregorian rules.

- lib/recurrence-rule.ts: createRecurrenceRule() helper with no skip/rscale.
- CalendarRecurrenceRule.rscale/skip are now optional.
- event-modal + recurrence-editor build rules via the helper.
- demo fixture cleaned; unit test asserts no skip/rscale.

Closes #805.
2026-08-25 21:13:34 +02:00
Linus Rath a15af72296 fix: resolve shared-account identity and email on reschedule; isolate per-account scheduled-list failures 2026-08-25 20:26:48 +02:00
Linus Rath 1305d13a77 Merge pull request #874 from rotterp/fix/scheduled-send-shared-accounts
fix: route scheduled sends to the account that owns the submission
2026-08-25 20:23:58 +02:00
Linus Rath 55ef780342 fix: cap live SSE streams per tab so many logins can't starve JMAP requests #702 2026-08-25 20:03:29 +02:00
Linus Rath af5fac6baa fix: flip $junk/$notjunk keywords on spam and not-spam #850 2026-08-25 19:36:52 +02:00
Linus Rath 3ef2b45fb4 feat: configurable message-list ordering #718 2026-08-25 19:20:48 +02:00
Linus Rath 9f32fc879b feat: add custom time zone setting overriding browser detection #755 2026-08-25 19:18:16 +02:00
Linus Rath 46ccd2267d fix: pin supported-calendar-component-set on created calendars #760 2026-08-25 19:16:20 +02:00
Linus Rath 8cd5c00b06 fix: keep the folder tree when a refresh burst hits maxConcurrentRequests #780 2026-08-25 19:09:21 +02:00
Linus Rath 8a7f8b0493 fix: gate first-touch calendar/contacts requests to stop duplicate default calendars #907
Stalwart lazily creates an account's default calendar on the first request
that touches it. Bulwark's login fires Calendar/get and CalendarEvent/query
concurrently, and on multi-node servers each node can create its own default,
leaving users with two identical "Personal" calendars. Hold later calendar and
address-book requests per account until the first one settles.
2026-08-25 19:07:01 +02:00
Linus Rath 5109aa059a fix: decode percent-encoded FileNode names from WebDAV-created nodes #869 2026-08-25 18:47:22 +02:00
texchi2andClaude Fable 5 5e0efff091 fix: toast store crashes on insecure origins (crypto.randomUUID undefined), breaking every post-action acknowledgement
addToast() minted toast ids with crypto.randomUUID(), which only exists in
secure contexts (https/localhost). On a plain-http LAN origin every
toast.success/toast.error threw a TypeError, so e.g. calendar event Save
crashed AFTER a successful CalendarEvent/set — before the dialog-close ran —
leaving the dialog open reporting failure while the server had the event;
retries created duplicates. Same class as the generateMessageId send bug.

Also guards the same-class contact uid fallback in lib/jmap/client.ts
(createContact). Both now use the guarded generateUUID() from lib/utils.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtFkcUA8VM2XAKZKzptpqy
2026-08-22 19:58:18 +08:00
texchi2 a92c852cc8 fix: generateMessageId crashes on insecure origins (crypto.randomUUID undefined) — use guarded generateUUID(); send failed after draft save, before EmailSubmission 2026-08-22 18:09:59 +08:00