Commit Graph
403 Commits
Author SHA1 Message Date
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
Linus Rath 93b8d99ff9 fix(): refill missing managed bundles from the server #636 2026-09-07 16:52:20 +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 6632834767 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-09-01 23:25:30 +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
Shuki Vaknin 12f01409a6 feat(search): add "clear search when switching folders" setting
Switching folders currently keeps an active search applied and re-runs
it in the newly selected folder — deliberate behavior from #553, where
users wanted the query to follow them across folders. Others expect the
opposite: clicking Inbox/Sent should drop the search and just browse the
folder.

Add an opt-in setting (Settings → email behavior) that reconciles both.
Default is off, so #553's search-follows-you behavior is unchanged for
everyone. When on, any folder click clears the text query and advanced
filters and browses the folder instead of re-running the search.

Applied uniformly across the four folder-entry paths in MailApp — normal
mailboxes, per-account (Pro sidebar) folders, unified views, and cross
views — via one helper so no path diverges.
2026-09-01 21:26:08 +03:00
Linus Rath a210b22e49 fix: set name.full on all contact write paths so vCards carry mandatory FN #430 2026-08-31 23:53:24 +02: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 a88b1e0848 fix: keep the two-argument deleteMailbox call when not removing messages 2026-08-29 18:27:01 +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 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 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 39dcc6b219 fix: invalidate persisted update status after an upgrade 2026-08-29 12:46:39 +02:00
Linus Rath d800689924 feat: use Stalwart synthetic ids for recurring occurrences #140 2026-08-29 11:03:48 +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 99daa53c8c feat: search suggestions with recent searches and contact autocomplete #845 2026-08-25 20:09:18 +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 a29cf27ff8 fix: route multi-account email lookups and invitation parsing by source account #847 2026-08-25 18:50:50 +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
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
Linus Rath 93fd9068f7 Merge pull request #863 from shukiv/feat/mobile-swipe-actions
feat(mobile): swipe list rows left/right for quick actions
2026-08-20 21:59:15 +02:00
Linus Rath 3e5ed4f3c4 feat: folder tabs via drag & drop + sidebar in fullscreen email tabs 2026-08-20 15:06:45 +02:00
Linus Rath 54d7fb6fc9 fix: folder drag & drop can move folders into other parents #855 2026-08-20 14:21:41 +02:00
Patrick Rotter 4461935a3c fix: route scheduled sends to the account that owns the submission
A delayed send composed from a shared/group identity is created in the
*shared* JMAP account, because sendEmail() passes the target account to
getSubmissionAccountId(). Every read/write on the scheduled surface,
however, asked the primary submission account only:

    getScheduledEmails / cancelEmailSubmission / rescheduleEmailSubmission
        -> getSubmissionAccountId()   // no argument = primary account

So a message scheduled from a shared address is accepted and stays
pending on the server — it *will* be delivered at sendAt — while the UI
cannot list it, cancel it or reschedule it. Cancelling appears to work:
EmailSubmission/set against the primary account answers `notFound` in
notUpdated, which the client never surfaced, and the mail goes out
anyway. Silent unrecallable mail is worse than a failed send.

Stalwart advertises urn:ietf:params:jmap:submission (with FUTURERELEASE
and a non-zero maxDelayedSend) on group accounts as well as personal
ones, so scheduling from a shared address is legitimate.

Data layer:
- getSubmissionAccountIds() enumerates every submission-capable account;
  getScheduledEmails() queries each, tags every result with the account
  that owns it, and fetches each submission's Email from that same
  account (it lives beside the submission, not in the primary account).
  It also returns per-account totals computed over all pending
  submissions, so a badge stays correct past the first page.
- resolveSubmissionAccountId() locates a submission's owning account so
  cancel/reschedule (and the sendAt/envelope helpers) act on the right
  one. Callers that know the account pass it and skip the lookup.
- sendEmail() reports the submission's account in SendEmailResult, and
  the pending-undo record carries it, so the "Undo send" / "Send now"
  toast actions — which know only a submission id — address the right
  account directly rather than searching for it inside the undo window.
- The delayed-send gate considers any reachable submission account
  rather than only the primary one.

UI:
- Each shared account gets its own "Scheduled" sidebar row, scoped to
  that account; the own row keeps showing the combined view. The scope
  travels beside the virtual mailbox id, so the store's single
  __scheduled__ id is unchanged, and it also drives which row renders as
  selected and whether the scoped list has more to load.

Tested against a real Stalwart (integration/tests/11-shared-scheduled-send):
two server-truth tests pin the mechanism (the submission is invisible to
the primary account, and cancelling through it is a silent no-op) and two
UI tests cover the fix. All three UI-level assertions — the combined
list, the scoped row, and which row is selected — fail without this
change.
2026-08-20 11:23:23 +02:00
Shuki Vaknin 78c84f19a5 feat(mobile): swipe list rows left/right for quick actions
Adds Gmail-style horizontal swipe actions to message rows on touch
devices. Swiping a row past a threshold fires a configurable action and
snaps back; a coloured background with an icon is revealed under the
finger and grows when the release would commit.

- New `useSwipeActions` hook: claims a touch as a swipe only once the
  horizontal travel dominates (vertical stays native list scrolling via
  `touch-action: pan-y`, no preventDefault), resists past the commit
  distance, and suppresses the tap that would otherwise follow.
- Two settings (Settings -> reading/email behavior): "Swipe right
  action" (default Archive) and "Swipe left action" (default Delete),
  each one of none / archive / delete / toggle read / toggle star /
  spam. A direction set to an action the current folder does not expose
  simply does nothing.
- Composes with the existing long-press (context menu) handlers; desktop
  is unchanged (hover actions + right-click).
- Scoped to single-message rows; unit tests cover commit/short-drag/
  no-action-side/vertical-scroll/tap-suppression.
2026-08-19 02:18:01 +03: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
KazNIISA IT 5c484af1f7 fix(jmap): escape the JSON Pointer in keyword patches, and migrate a keyword honestly
A PatchObject key is a JSON Pointer (RFC 8620 section 5.3), so a slash
inside the member name it points at has to be escaped as `~1`. Keyword
patches were built as `keywords/${keyword}` verbatim, which is fine for
every flat keyword but wrong for a nested tag: `$label:work/clients`
became the pointer `keywords/$label:work/clients`, asking for the
`clients` member of the `$label:work` keyword rather than for the keyword
itself. The patch is not a boolean at that path, so the server rejects or
ignores it and the tag never lands.

Route the keyword patch sites - setKeyword, removeKeyword, migrateKeyword
and the category-tab batch patch - through `keywordPointer`, which escapes
per RFC 6901 the way the calendar store already does for participant ids.
Flat keywords are untouched, so `$seen`/`$flagged` and friends keep the
exact pointers they had. The dev JMAP mock reads the escaping back, or
nested tags would behave differently against the mock than against a real
server.

migrateKeyword also took any answer as success: a method-level error on
the query read as "no message carries this keyword", one on the write was
ignored outright, and a per-message refusal was invisible - so a rename
that moved nothing still reported a clean migration. It now answers with
{ migrated, refused }, because the work is batched and a failure partway
through cannot undo the batches already applied. A whole batch the server
refuses stops the rest, which it would refuse too; a single message it
refuses does not, the mail behind it being unaffected. A query error
throws, nothing having been written yet, and so does a run that migrated
nothing at all - the case where the caller must not rename a tag over
messages that all kept the old keyword.

The walk over tagged mail was not paginating safely either. It stopped at
the first page shorter than the limit it asked for, but RFC 8620 section
5.5 lets a server clamp that limit, so a clamping server left most of the
mail unmigrated. Positions are also only comparable within one queryState,
which the walk ignored: mail arriving mid-walk shifts every later page. It
now restarts when the state changes under it, and gives up - before any
write, so the caller can simply repeat - once a mailbox has moved under it
three times, rather than mixing ids from states that no longer agree. A
page carrying no id it has not already seen ends the walk, which keeps it
bounded against a server that answers every position with the same page.
2026-08-18 08:15:22 +05: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 c78b964c69 perf: halve time-to-mail-list (lazy locale catalogs, code-split viewer/composer, shorter auth waterfall, boot snapshot) 2026-08-17 01:50:10 +02:00
Linus Rath 5b8ddc352b feat: pick push relay from admin-defined list instead of a URL field 2026-08-16 23:04:22 +02: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 e44f35a203 fix: reuse cached access token on session restore #552 2026-08-16 22:20:07 +02:00
Linus Rath fc0ab57247 Merge pull request #835 from hildebrandttk/fix/hide-tasks-only-calendars
fix(calendar): hide tasks-only calendars from the event calendar UI
2026-08-16 21:59:36 +02:00
Linus Rath 034b28598f feat: render plain-text emails in the app font by default, with a monospace option #830 2026-08-16 21:55:31 +02:00
Linus Rath d8c9aaa66a feat: add setting to render plain-text emails in normal or monospace font #830 2026-08-16 21:54:52 +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
Stefan Hildebrandt 9c084d9d63 fix(calendar): hide tasks-only calendars from the event calendar UI
A calendar holding only tasks (VTODO, e.g. from tasks.org) appeared in the
event calendar UI as an empty calendar. Stalwart's Calendar/get exposes no
supported-component set, so tasks-only-ness is derived from the calendar's
objects (mirroring the existing event/task split): a calendar is tasks-only
iff it holds >=1 object and every object is a task.

- getTasksOnlyCalendarIds scans an account's objects (CalendarEvent/query +
  minimal /get), early-exits once every calendar has an event, only marks
  after a full scan, and fails open on the cap or any error.
- getAllCalendars/getCalendars annotate calendars with isTasksOnly.
- Such calendars stay in the store for the tasks view (task picker, task-list
  grouping) but are filtered from the event sidebar and default selection.

Verified against Stalwart v0.16.16: the tasks-only calendar is flagged, the
default (with an event) is not.

Closes #761.
2026-08-16 21:18:57 +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 8e3b4e71a1 fix: open right conversations in shared mailbox #814 2026-08-16 02:19:04 +02:00
Linus Rath e6597542fd fix: include email templates in cross-device settings sync #825 2026-08-16 02:09:38 +02:00