Commit Graph
20 Commits
Author SHA1 Message Date
Linus Rath 9de2efb722 fix: open a shared or group account's message from its link instead of the own message with the same id #1351 2026-10-26 20:01:11 +01:00
Linus Rath 41a0716c50 Merge branch 'main' into feat/stalwart-1.0 2026-10-16 01:41:48 +02:00
Linus Rath 4d733aa598 chore: remove unused exports and the dead plugin error boundary 2026-10-13 13:36:51 +02:00
Linus Rath c633185cda Merge branch 'main' into feat/stalwart-1.0
# Conflicts:
#	CONTRIBUTING.md
#	LICENSE
#	README.md
#	app/api/calendar-agenda/route.ts
#	app/api/wopi/files/[fileId]/contents/route.ts
#	app/api/wopi/launch/route.ts
#	components/calendar/calendar-app.tsx
#	components/calendar/calendar-day-view.tsx
#	components/calendar/calendar-month-view.tsx
#	components/calendar/calendar-week-view.tsx
#	components/calendar/event-modal.tsx
#	components/contacts/contact-detail.tsx
#	components/email/email-composer.tsx
#	components/email/email-list.tsx
#	components/files/file-browser.tsx
#	components/files/files-app.tsx
#	components/files/folder-tree-sidebar.tsx
#	components/filters/filter-rule-modal.tsx
#	components/mail/mail-app.tsx
#	hooks/use-mailbox-drop.ts
#	integration/run-tests.sh
#	lib/__tests__/calendar-agenda-route.test.ts
#	lib/__tests__/jmap-draft-lifecycle.test.ts
#	lib/__tests__/web-push.test.ts
#	lib/account-state-manager.ts
#	lib/jmap/client-interface.ts
#	lib/jmap/client.ts
#	lib/jmap/types.ts
#	lib/message-list-order.ts
#	lib/sieve/generator.ts
#	lib/sieve/parser.ts
#	lib/telemetry/payload.ts
#	lib/thread-utils.ts
#	lib/unified-mailbox.ts
#	lib/web-push.ts
#	lib/wopi/token.ts
#	locales/ar/common.json
#	locales/ca/common.json
#	locales/cs/common.json
#	locales/da/common.json
#	locales/de/common.json
#	locales/en/common.json
#	locales/es/common.json
#	locales/fa/common.json
#	locales/fr/common.json
#	locales/he/common.json
#	locales/hu/common.json
#	locales/it/common.json
#	locales/ja/common.json
#	locales/ko/common.json
#	locales/lv/common.json
#	locales/mn/common.json
#	locales/nb/common.json
#	locales/nl/common.json
#	locales/pl/common.json
#	locales/pt/common.json
#	locales/ro/common.json
#	locales/ru/common.json
#	locales/sk/common.json
#	locales/tr/common.json
#	locales/uk/common.json
#	locales/zh-TW/common.json
#	locales/zh/common.json
#	scripts/lite/lib.mjs
#	stores/__tests__/calendar-store-synthetic-ids.test.ts
#	stores/__tests__/email-store-shared-folder-keywords.test.ts
#	stores/__tests__/email-store-unified-actions.test.ts
#	stores/__tests__/file-store.test.ts
#	stores/email-store.ts
#	stores/file-store.ts
#	stores/message-list-tabs-store.ts
2026-10-12 08:35:08 +02:00
Linus Rath 7b61e55529 chore: write nul separators as \0 so git diffs these files as text
Three files carried a literal NUL byte inside a string: the admin
check's cache key in `app/api/admin/auth/route.ts`, the global search
hit key in `lib/global-search/types.ts` and two TNEF fixtures in
`lib/__tests__/tnef.test.ts`. Git treats any file containing a NUL as
binary, so it showed no diffs for them, never normalised their line
endings, and reported every pull request that touched the admin route
as a conflict even when a text merge was clean.

The escape produces the same string at runtime. The admin route was
stored with CRLF while git saw it as binary; it is now stored with LF
like the rest of the tree, so its diff covers the whole file (the only
content change is on line 70).
2026-09-30 19:34:37 +02:00
Linus Rath 763732fdc9 feat: leave spam and trash out of search by default 2026-09-29 19:52:39 +02:00
Linus Rath 219ea22276 fix: act on selected messages of expanded threads in their own account
In a unified view, a tag view or an "All folders" search, a message can
be selected inside an expanded thread without being a row of the list.
The batch actions only looked at the rows:

- "Delete" and "Mark as read" sent such a message's id without its
  account, so the write went to the account the list is reached
  through, where the same id can name another message: an unrelated own
  message could end up in Deleted Items. They now write it in its own
  login and account, as they do the rows, and a message marked read
  shows as read in its thread.
- "Archive" and "Move to" skipped it without a word. It is archived or
  moved with the rest now.
- A selected id that is neither a row nor a message of an expanded
  thread is left alone, stays selected, and the error says so. The
  messages of a thread collapsed again no longer count, for "Spam" and
  "Not spam" either.

A mail hit of the active login opened from search in the Pro shell was
read by its id through the mail list, which may show another login, or
another account's message with the same id. It is read through the
hit's login and account now, as other logins' hits already were.
2026-09-26 04:56:23 +02:00
Linus Rath aa99f038d1 fix: keep a scheduled message when its new time loses a recipient
"Send now" and "Reschedule" create a replacement submission and then
cancel the original. The server checks the recipients again for the
replacement, and a throttle used up since the original was taken ("452
4.4.5 Rate limit exceeded") can refuse some of them. The original was
kept only when every recipient was refused; on a partial refusal it was
cancelled, and a recipient the original had taken lost the message
without anyone being told.

- The original's delivery status is read with its envelope. When the
  replacement refuses a recipient the original took, the replacement is
  cancelled (its record only destroyed when it took nobody) and the
  original goes at its old time. A recipient the original refused as
  well loses nothing, so the reschedule goes ahead; checked live on
  0.16.23 and 1.0 with a relay-refused recipient.
- The toast for a kept schedule leads with "The message is still
  scheduled for ..." and says "Nothing was changed: the server refused
  ...". It used to open with "Not sent", which invited sending the
  message a second time.
- Recipients refused while the others were taken are now named after
  "Send now", a reschedule from the list or the viewer, and a quick
  reply, which said "Email sent".

Files, from the review of d9ffe8a7:

- An own folder opened from search while Files showed a shared folder
  opened the shared folder again: its link had no query, and the page
  read `?shared=` from the address bar. The link now says it has none.
- The Pro shell kept only the path of a Files link across its redirect,
  so a shared folder's link (`/files?shared=...`) didn't survive a
  reload, and neither did `?preview=`. The query goes along now.
- A search hit for a file on a "Files from" page opened the first file
  of that name there; it is found by the id the link names now.
- Undoing a paste of files cut on a "Files from" page moved them to
  "*", the page's id; they go back to their own folders now.
2026-09-26 02:25:40 +02:00
Linus Rath d9ffe8a76e fix: reach directly shared files and keep others' files on a failed copy
Fixes from the review of the Files work (1cf9944b..a9a25b94).

- A file shared on its own, or one below a folder its owner took back,
  could not be reached. Once Stalwart 1.0's placeholder folders were
  left out it became a root of "Shared with me", which listed folders
  only, and search opened it on the drive root. 0.16 lists such files
  as roots too. Each owner's files shared on their own are now listed
  on one page under "Shared with me" ("Files from <name>"), where they
  open, preview and download like any other file and nothing can be
  added; two of the same name get a number. Search opens a shared hit
  where it is listed, and links into a shared drive name the node by id
  (`/files?shared=<owner>:<id>`), so a shared folder or page also
  survives a reload. A Files page that is already open takes the link
  from search directly, since Next keeps it mounted when only the query
  changes.
- A copy that fails part-way destroys only the nodes it made, the
  deepest first and without `onDestroyRemoveChildren`, one call per
  level because 0.16 refuses a folder destroyed in the same call as
  its children. It used to destroy the new folder with everything
  below it, so whatever someone put there during a long copy was
  deleted for good. A folder holding such nodes now stays, and the
  error says part of the copy was left in place.
- The document editor opens read-only on 0.16 for a sharee without the
  rename right. 0.16.6 to 0.16.23 refuse any change a sharee makes
  without Modify, so every save of such a document failed with a 500.
  1.0, told apart by its `maxFileNodeDepth`, still asks for
  `mayModifyContent` alone.
- Ctrl+C with only symlinks selected empties the clipboard instead of
  keeping an earlier cut for the next paste.
- A paste reads each account's file tree once, not once per pasted
  item for the listing and 1.0's depth check.
- The Stalwart bundle's README says that on older 0.16 releases and
  the 1.0 pre-releases a failed update also drops the cached copy, and
  the next restart or reload of the Applications downloads it again,
  not only a later `UpdateApps`. The static README says a restart
  applies the CORS setting on the current 1.0 pre-releases, where
  `ReloadSettings` fails with the stock spam-filter rules.
2026-09-25 22:16:19 +02:00
Linus Rath 60c2dbdcb4 fix: reload a failed calendar range and expand multi-word event searches
Four smaller calendar fixes found in review of the Stalwart 1.0 work.

- A range load that failed left `dateRange` on the range loaded before,
  so a push or a pull-to-refresh reloaded that one instead of the range
  on screen, and any action that reset `error` hid the "may be out of
  date" notice. The store now takes the range asked for and keeps the
  failure in `eventsLoadError`, which only a successful range load
  clears.
- The agenda plugin's expanded query had no sort, so its limit could
  keep later occurrences instead of the next ones. It now sorts by
  start, looks less far ahead when the server refuses the range as too
  long (`expandDurationTooLarge`, an administrator's shorter
  `maxExpandedQueryDuration`) instead of answering 502, and sends its
  gets several per request.
- With a limit (search, contact activity) the chunks of a long range
  went out one request each. Up to four now share a request, and what
  the later ones return past the limit is dropped. Global search passes
  its AbortSignal down, so a replaced search stops between requests.
- A search of several words sends its date bounds as conditions of a
  flat AND. The client did not see them as a range: it neither expanded
  recurrences nor turned the UTC bounds into the request's wall clock,
  so such a search found series instead of occurrences, over a range
  shifted by the zone's offset. The bounds are now lifted out of the
  AND, converted and chunked, and each chunk keeps one flat AND of
  single conditions - the shape 1.0 expands; it refuses an AND holding
  a condition with both bounds.
2026-09-25 19:42:38 +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 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 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 cee760e992 fix: send mail search terms as typed instead of with a fake prefix wildcard 2026-09-22 21:04:18 +02:00
Linus Rath b5b18371a4 fix: drop unused symbols left by the search dedupe change 2026-09-01 22:16:58 +02:00
Linus Rath bc3b6a0eb6 fix: collapse search hits for the same server object reached through several logins #641 2026-09-01 22:15:49 +02:00
Linus Rath 1d8e4b9d8a fix: duplicate calendar-app import and type errors the stale tsbuildinfo hid 2026-09-01 21:08:10 +02:00
Linus Rath 0c7437f67a feat: open global search hits through the owning login on every surface #847 2026-09-01 20:55:13 +02:00
Linus Rath 0db6bfdc64 feat: global search core: query parser, ranking, cross-account providers #641 2026-09-01 20:55:12 +02:00