Commit Graph
100 Commits
Author SHA1 Message Date
Ctaoist fc812c9b23 fix: preserve label filtering during mailbox refreshes 2026-09-12 09:40:44 +08:00
Linus Rath fb24ef5d24 feat: sort contacts by last name #963 2026-09-09 19:47:33 +02:00
Linus Rath c395226926 fix: reject wrong passwords server-side so the browser never shows its Basic Auth dialog #969 2026-09-09 19:21:40 +02:00
Linus Rath d4d3f317a5 fix: stop retrying token refreshes that fail permanently #972 2026-09-07 22:34:47 +02:00
Linus Rath 80dadfbbd4 fix: open search hits from shared folders in the right account #923 2026-09-07 22:34:47 +02:00
Linus Rath 150c25ca0e fix: mark as spam from the viewer when the message left the list #695 2026-09-07 22:34:47 +02:00
Linus Rath 8dc1564b72 fix: archive shared-inbox mail into the owner's archive #889 2026-09-07 22:34:47 +02:00
Linus Rath 2b32a0110b fix: report a missing archive mailbox instead of failing silently #578 2026-09-07 22:34:47 +02:00
Linus Rath 0e7095e509 Merge pull request #872 from vj1235432/fix/quick-search-stale-responses
fix(search): guard quick search against stale responses
2026-09-07 17:56:54 +02:00
Linus Rath 7652d974e1 Merge pull request #860 from dealerweb/fix/logout-test-force-url
test: match the forced token refresh URL in the logout tests
2026-09-07 17:43:22 +02:00
Stefan Hildebrandt 242fe6aa2d fix(mail): unified mailbox missed accounts that had not connected yet
After a reload the unified mailbox showed only the active account's mail;
other accounts appeared only once visited. Intermittent (~1 in 14), and
not a timeout - nothing on this path uses setTimeout/AbortSignal/race.

checkAuth awaits only the active account and restores the rest in a
fire-and-forget background loop, registering each client only after
connect() resolves. buildUnifiedAccountClients drops accounts with no
live client (`if (!c) continue`). And isConnected was persisted, so it
rehydrated stale-true: the account passed the isConnected filter while
failing the client lookup, and connectedAccountsSignature already looked
complete at mount so nothing re-fired - and the effects keyed on it only
recompute counts, never the list.

Clear isConnected/hasError on rehydrate so both the filter and the
signature describe this session, and refetch the open unified/cross list
when an account joins the connected set (growth only, not on first
render, not while a search owns the list). Extracted
connectedAccountsGrew so the membership check is testable without
rendering MailApp.

Verified the reset cannot block reconnection: checkAuth reads the full
account list and never filters on isConnected.

Fixes #950
2026-09-02 22:15:50 +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
Linus Rath 0b39e43a4d feat: surface ShareNotification changes as toasts 2026-08-29 17:59:04 +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 99daa53c8c feat: search suggestions with recent searches and contact autocomplete #845 2026-08-25 20:09:18 +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 f189e0bab6 fix: refresh account display name from the Stalwart principal on login/restore/switch #900
Stalwart seeds the default identity name from the principal "Full name" only
once, and addAccount never rewrote displayName after first login, so an admin
rename never showed up in Bulwark. Read the live name via x:Account/get
through the passthrough (falling back to the identity name elsewhere) and
prefer the synced registry name in account settings.
2026-08-25 18:45:44 +02:00
Linus Rath 3e5ed4f3c4 feat: folder tabs via drag & drop + sidebar in fullscreen email tabs 2026-08-20 15:06:45 +02:00
vj1235432andClaude Opus 5 88712994de fix(search): guard quick search against stale responses
Quick search fired one request per keystroke with no AbortController and no
staleness check, so whichever response landed last won regardless of which
query it belonged to. Typing an address slowly showed hits belonging to a
short prefix while the box held the full string; retyping the same string in
one go correctly showed none, which reads as "search works sometimes".

advancedSearch already guarded every write with controller.signal.aborted.
This gives searchEmails the same treatment, reusing the existing
searchAbortController so the two search paths supersede each other rather
than race. The catch branch is guarded too: without it a superseded request
that rejects clears results belonging to the current query and surfaces a
misleading error.

Adds stores/__tests__/email-store-search-race.test.ts with three cases, all
of which fail on main and pass with this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:48:54 +04:00
dealerweb 7aa3825be7 test: match the forced token refresh URL in the logout tests
The shorter auth waterfall makes refreshAccessToken skip the server-side
token cache by default, appending force=true to the PUT. The logout
redirect tests still mocked the bare slot URL, so every refresh hit the
mock's unexpected-call guard and three tests failed. Match the PUT on
the forced URL; the DELETE paths are unchanged.
2026-08-18 15:28:02 +02:00
Linus Rath 58ddd01cf7 Merge pull request #792 from dev-hive-kazniisa/fix/shared-mailbox-folder-management
fix(email-store): route shared-folder management to the owner account
2026-08-17 12:18:24 +02:00
KazNIISA IT ad0cc108b4 fix(email-store): scope the owner-client lookup to one server
Review follow-up on the shared-folder mutation routing.

The owner lookup replaced the resolved client with any connected client whose
JMAP account id matched. Account ids are opaque per-server values, and personal
mailboxes carry an accountId too, so this ran on every folder mutation and could
hand a mutation to a client of a different server that happens to issue the same
id - where the bare mailbox id may exist and belong to someone else. Keep the
resolved client when it already is the owner, and require a candidate to match
the server URL as well as the account id.

Recover the owner account and the bare mailbox id from the namespaced store id
when no mailbox object is available. Previously the raw `accountId:id` went to
the server as a mailbox id with the owner dropped, so the mutation targeted the
active personal account; in setMailboxRole that cleared the personal role before
failing.

Drop the shared account header context menu when the account carries no usable
id. buildMailboxTree groups shared mailboxes without an accountId under the
placeholder "unknown", which cannot be a mutation target.
2026-08-17 09:10:11 +05:00
Linus Rath 644a9a0793 fix: search folder filter defaults to all folders and persists #788 2026-08-16 22:32:14 +02:00
Linus Rath ffff165a4b Merge pull request #800 from rotterp/fix/shared-folder-account-context
fix: a directly-selected shared folder has no account context (tags, compose identity)
2026-08-16 21:27:18 +02:00
Linus Rath 47cf0ed90d feat: rework pro split-screen shell, per-pane tab strips, drag & drop, pane-scoped overlays 2026-08-16 03:03:40 +02:00
Linus Rath e6597542fd fix: include email templates in cross-device settings sync #825 2026-08-16 02:09:38 +02:00
Stefan Hildebrandt b63b227645 fix(email): route unified/cross virtual ids through the fan-out in fetchEmails (#791)
fetchEmails sent a unified/cross view's client-only virtual mailbox id
(__cross_all__ / __unified_*) straight to getEmails as `inMailbox`. The server
rejects it (Stalwart: "filter.inMailbox = '__cross_all__'") and the foreground
catch wiped the list to "no messages found" - notably after actions (mark read,
delete, move) in the aggregated views.

Mirror the loadMoreEmails / refreshCurrentMailbox guard: when the target id is
virtual, fan out via fetchCrossViewEmails / fetchUnifiedEmails (search-aware) at
page 0 instead of querying the virtual id. If the view state isn't resolved yet,
stop loading without sending the id or wiping the list. Real folders are
unaffected.

+5 unit tests (email-store-fetch-unified): cross/unified fan-out, explicit
virtual-id arg, unresolved-view no-op, and the real-folder path still using
getEmails. tsc + eslint clean; all store suites green (389).
2026-08-13 21:27:24 +02:00
Patrick RotterandClaude Opus 5 3560cf258b fix: route keyword writes to the selected shared account
Tags, pins and $answered/$forwarded/$mdnsent set on a message in a
shared/group mailbox opened directly from the "Shared" sidebar section
did not persist: they showed until the next reload and were then gone.

The call sites resolved the target account as
`isUnifiedView ? email.sourceAccountId : undefined`. Outside the unified
view that is always `undefined`, so the write went to the reaching
client's own account, which the server answers with `notUpdated` /
`updated: null` and no error - the same silent no-op that
`email-store-shared-folder-actions.test.ts` already covers for the batch
actions. Emails fetched from a shared folder this way are undecorated
(no `sourceAccountId`); the owner is only recoverable from the selected
mailbox.

`toggleStar` never had the bug because it routes through
`resolveEmailActionContext`, which resolves the owner from the email's
source in an aggregate view and from the selected mailbox otherwise.
That resolver was reimplemented inline at each keyword call site, and
the inline copies only ever covered the aggregate half - so starring a
shared message persisted while tagging the same message did not.

Move the two keyword writes into store actions built on that resolver
and route every call site through them, including the read-receipt and
Pro-compose paths that previously passed no accountId at all.

The store actions resolve only for an email the store actually knows.
A Pro tab fetches its own email and never enters `emails` or
`selectedEmail`; for those the selected mailbox says nothing about the
message's owner, so guessing from it would send an own-account write to
a shared account whenever a shared folder happened to be open. An
unknown email keeps the previous behaviour instead.

Behaviour on own-account and unified-view messages is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 10:42:06 +02:00
Linus Rath 70383dea18 Merge pull request #796 from lucas-gaitzsch/fix/scheduled-send-polish
fix: polish scheduled send follow-ups
2026-08-12 18:12:28 +02:00
Lucas Gaitzsch 17f18e378e fix: polish scheduled send follow-ups 2026-08-12 17:46:51 +02:00
Ctaoist f1fdb24c8c feat: emit mailbox refresh hook 2026-08-12 22:08:09 +08:00
KazNIISA IT c516ba98d0 fix(email-store): route shared-folder management to the owner account
Folder management performed on a shared/group account was sent to the
logged-in user's primary JMAP account instead of the mailbox owner's
account. Subfolder creation therefore failed with invalidProperties for
parentId, root folders could be created silently in the personal account,
and rename, delete, role, and reorder targeted the wrong mailbox tree.

Shared mailboxes use namespaced store ids to avoid cross-account
collisions, while Mailbox/set requires the owner's accountId and the bare
server mailbox id. Resolve that mutation context centrally: recover the
mailbox originalId and owner account, use a directly connected owner
client when one exists, otherwise reach delegated/group accounts through
the active client with an explicit accountId.

Thread the account target through create, update, and delete JMAP methods
and all folder-management actions. Scope role replacement to the same
owner account, and expose explicit root-folder creation from shared
account headers without making personal root creation depend on the
currently selected folder.

Add unit coverage for JMAP request scoping, shared store routing, role
isolation, direct-client handling, and reorder. Add a Stalwart integration
scenario that creates root and nested group folders, renames them, and
deletes them through the UI.
2026-08-12 18:11:43 +05:00
Linus Rath 5e73633a80 Merge pull request #777 from hildebrandttk/fix/consistent-cross-account-id-namespacing
Fix/consistent cross account id namespacing
2026-08-11 17:37:55 +02:00
Stefan Hildebrandt 0efdb95a27 fix(settings): merge per-account maps on server load (compose identity switch)
With multiple accounts and settings sync on, the composer's default sender
would switch depending on login order and get disturbed by logging a third
account off. Cause: the per-account maps preferredIdentityIds (#507) and
allMailFolderIds live in EVERY account's synced settings blob, and
loadFromServer(username, serverUrl) runs once per connected account calling
importSettings(), which REPLACED the whole map with that account's server copy.
So the last account to load clobbered the map (last-writer-wins by login
order), and a stale blob from a since-removed account could win too.

Fix: importSettings takes an optional { serverAccountId }; loadFromServer passes
generateAccountId(username, serverUrl). For the per-account maps, a server load
now merges only that account's OWN entry into the existing map instead of
replacing it - each account's server is authoritative only for itself, so other
accounts' entries survive. File imports (no serverAccountId) still replace
wholesale.

+ regression tests: a per-account load preserves other accounts' entries,
only trusts the loading account, file import still replaces, same for
allMailFolderIds. tsc + eslint clean.
2026-08-10 22:49:35 +02:00
Stefan Hildebrandt 583e20d9dc fix(calendar,contacts): namespace all accounts consistently on switch
Calendar/contact aggregation left the ACTIVE account's ids raw while
namespacing background accounts (<localAccountId>::id). So an entity's id
depended on which account was active; switching flipped the id form and
invalidated persisted references (selectedCalendarIds), broke isSubscription
matching, and destabilised selection - the account-switch bug users saw in
Calendar and Address Book (mail was fine: it keys per account + stable
virtual ids).

Invariant now: an entity is namespaced iff it carries a localAccountId;
originalId always holds the raw JMAP id. Consumers/mutations already resolve
the raw id via `originalId || stripLocalAccountPrefix`, so they are unchanged.

- prefix* functions (calendar + contacts) always namespace + set originalId;
  drop the isActiveAccount branch and the now-dead activeLocalAccountId params
  (callers in the hooks + calendar-app/contacts-app updated).
- mapServerEventToStoreEvent namespaces the event id with the matched calendar's
  localAccountId, so createEvent/updateEvent results are consistent.
- reconcileSelectedIds(): remap selectedCalendarIds by raw identity on every
  fetch, so a raw->namespaced transition (switch, just-created calendar, or the
  one-time migration from old persisted raw ids) keeps the selection instead of
  resetting to "all".
- isSubscriptionCalendar(): namespace-robust and collision-safe - resolve the
  calendar and match by raw id scoped to the owning JMAP accountId (Stalwart's
  raw ids like "b"/"c" collide across accounts).

+9 unit tests (selection stability across switch/create, subscription
robustness, no cross-account false positives). tsc + eslint clean; 368 store
tests green.
2026-08-10 22:45:21 +02:00
Stefan Hildebrandt 7a3a262c87 fix(email): keep just-read/unstarred mail in the open unread/starred view
In the unread ('unread') and starred ('starred') cross views, marking a mail
read or removing its star flips the very keyword the view filters on
(notKeyword:$seen / hasKeyword:$flagged). The push-triggered refresh re-runs
that filtered query, so the mail was absent from the fresh page and the merge
dropped it - the row vanished from under the cursor on the user's own action.

Keep such rows in place: only their status + the counters update (both already
applied by markAsRead/toggleStar/batchMarkAsRead), they stay visible until the
view is re-opened. A new retainedInViewIds set records the ids the user just
read/unstarred while in these views; refreshCurrentMailbox re-inserts those rows
at their prior position via mergeRetainedRows. The set is scoped to the two
keyword cross views, cleared on navigation (fetchEmails/fetchUnifiedEmails/
fetchCrossView/exitUnifiedView), and dropped per-id when the action is reversed
(mark unread / re-star). totalEmails counts the retained rows so hasMore stays
correct.

Genuinely removed rows (deleted/moved -> absent from the local list) are not
resurrected, and a change the user did NOT make in this view (e.g. read on
another device, retain set empty) still drops as before.

Tests: stores/__tests__/email-store-retained-in-view.test.ts - read keeps the
row (unread view), unstar keeps the row (starred view), reversing/ navigation
clear retention, external read still drops, 'all' view does not retain. The
row-keeping cases fail without the fix.
2026-08-09 22:17:50 +02:00
Stefan Hildebrandt 2a1585a4d3 fix(email): stop All-Mail views emptying on delete/star/read
refreshCurrentMailbox runs on every Email state push (delete, star, and
mark-read all raise one). It only handled real single mailboxes: in a
unified ("All Mail") or cross-account view, selectedMailbox is a virtual
id (e.g. __cross_all__ / __unified_inbox__) that no folder matches, so it
was sent to getEmails as an inMailbox filter, returned an empty page, and
the first-page merge wiped the whole aggregated list. The empty query was
previously masked by the length-based merge cutoff; af2b00dd (#592)
changed the cutoff to the page size and unmasked it.

Make refreshCurrentMailbox view-aware, mirroring loadMoreEmails /
performSearch: fan out page 0 via fetchCrossViewEmails / fetchUnifiedEmails
(search-aware) when in those views, then run the existing merge so loaded
pages are preserved. Surface cross-view fetch errors via unifiedErrors.

Test: email-store-refresh-unified asserts a push refresh in the cross-All
and unified-inbox views keeps the list and never queries the virtual id
(fails without the fix: the list wipes to []).
2026-08-09 22:17:42 +02:00
Linus Rath 905e6ed96f fix: split "Name <addr>" recipients into the JMAP name and email fields #672 2026-08-06 19:19:18 +02:00
Linus Rath b3f5f51efa fix: stop minting duplicate "Trusted Senders" address books #730 2026-08-06 15:07:54 +02:00
Paulhenry Saux 3b519c3b3b feat: manage public keys and at rest encryption 2026-08-05 21:46:42 +02:00
Linus Rath e94a1429d5 Merge pull request #724 from paulhenry46/userlogout-api-hook
feat: implement existing hooks onBeforeLogout and onAfterLogout + new plugin API method
2026-08-01 23:09:39 +02:00
Paulhenry Saux d15c58f8da feat: implement logout hook and add a new plugin api method to perform logout 2026-08-01 21:46:33 +02:00
Linus Rath 348e032dce fix: files show creation date instead of modification date #700 2026-07-30 19:41:02 +02:00
Mathy Vanvoorden ea7892b497 feat: Change the dev mode defaults to include nested tags 2026-07-29 19:52:31 +02:00
Mathy Vanvoorden 108406a885 feat: improve visualization of tags
Previously tags where very much focused on color coding email and less about
adding additional information. They were also visualized in different ways in
different locations.

This commit gets rid of all "Color-coding" references, aligns visualization of
the tags across the whole project and tries to improve user experience of using
tags in general.

A search box is shown in the tagging control so the user can quickly search for
a tag if they have a huge (more than 10) amount of tags.
2026-07-29 17:18:42 +02:00
Mathy Vanvoorden 0c1e238223 feat: allow hidden tags, either permanent or when there are no unread messages 2026-07-29 17:18:42 +02:00
Mathy Vanvoorden ca0ba818b7 feat: Add nesting of tags in a tree
- levels are joined by forward slashes in the keywords
- behaviour is opt-in for now
- long paths are shortened if there is not enough display room

Closes #687.
2026-07-29 17:18:38 +02:00