Commit Graph
1802 Commits
Author SHA1 Message Date
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 e838794133 Merge pull request #895 from romanvru/feat/upload-progress
Real byte progress for attachment uploads, stock and plugin-offloaded
2026-08-25 18:55:07 +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 5109aa059a fix: decode percent-encoded FileNode names from WebDAV-created nodes #869 2026-08-25 18:47:22 +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 c65da9d76a Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-08-25 18:40:00 +02:00
Linus Rath 2e650a93b4 Merge pull request #888 from texchi2/fix/insecure-origin-randomuuid
fix: guard crypto.randomUUID for insecure origins (toast store, message-id, contact uid)
2026-08-25 18:39:24 +02:00
Linus Rath ed340cc2c1 feat: make Stalwart admin access to the dashboard configurable #870 2026-08-25 18:39:07 +02:00
Linus Rath 342217ad99 fix: keep read/unread toolbar button a fixed width so buttons don't jump on open #864 2026-08-25 18:16:51 +02:00
French-Asgeir e59b975464 fix: event toolbar overflow in edit mode 2026-08-24 02:57:25 +02:00
Roman Varlakov 7928f72ee2 feat: real byte progress for attachment uploads, stock and plugin-offloaded
The composer chip only knew a boolean: `uploading` flipped on, an
indeterminate bar animated at 40% width, and nothing anywhere carried a
percentage, so a large attachment on a slow link looked frozen for the
whole transfer. The JMAP client has been able to report real progress
since #333 gave uploadBlob an XHR path with onProgress; the composer
just never passed it.

Stock path: pass onProgress and the existing AbortController's signal to
client.uploadBlob. The chip's bar becomes determinate as soon as bytes
are reported (falling back to the indeterminate animation otherwise),
and cancel now aborts the transfer itself instead of being checked only
once the upload has finished.

Plugin offload path (#751): a plugin that offloads an attachment moves
the bytes itself via api.http.post, which the composer cannot observe.
It also cannot be handed a callback: the sandbox bridge marshals
functions host-to-plugin only, and the File a plugin holds is a
structured clone, so object identity cannot correlate its request back
to an attachment. The staged file id can: the composer generated it and
handed it to onBeforeBlobUpload. A plugin echoes it back as the new
optional `progressFileId` on the post options; the host sends the Blob
via XMLHttpRequest (fetch exposes no upload progress events) and reports
bytes into a host-side registry keyed by that id, which the composer
subscribes to before running the hook. Progress never crosses into the
sandbox, and reports for an id the composer is not tracking are dropped,
so the plugin API grows exactly one optional field. Calls without
progressFileId keep the fetch path untouched.

Closes #754
2026-08-23 19:44:56 +06:00
Linus Rath 9a19ddbbac Merge pull request #890 from paulhenry46/wkd
feat(plugins): new plugin API method: getPublicKeyFromWKD
2026-08-22 21:43:04 +02:00
Paulhenry Saux d82a8e5ca5 feat(plugins): new plugin API method: getPublicKeyFromWKD 2026-08-22 20:55:45 +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
Linus Rath eb9df28270 Merge pull request #886 from shukiv/feat/group-push-notifications
feat(push): group new-mail notifications per account instead of one per message
2026-08-21 23:16:43 +02:00
Linus Rath 5e27803573 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-08-21 18:05:32 +02:00
Linus Rath 7e9aeaf0d4 fix: calendar click/double-click created events near current time instead of the clicked slot 2026-08-21 18:04:33 +02:00
Shuki Vaknin d29ab9b552 feat(push): group new-mail notifications per account instead of one per message
Each email push showed a notification with a unique tag
("bulwark-mail:<id>"), and Android renders one notification per unique
tag — so a burst of mail stacked one toast per message (50 arrivals =
50 toasts).

Use a shared per-account tag ("bulwark-mail:<accountId>") so each push
replaces the account's single notification. The newest message stays the
headline and a Gmail-style "+N more messages" line, counted from the
account's unread total, carries the rest. A single unread still deep-links
to that message on click; a group opens the inbox.
2026-08-21 18:48:04 +03:00
Linus Rath a5d08b573a Merge pull request #862 from dealerweb/fix/signature-unlock
fix: let a double-click unlock the embedded signature for editing
2026-08-21 09:26:50 +02:00
Linus Rath 6052f32129 Merge pull request #876 from dealerweb/fix/sent-draft-stale-viewer
fix: clear the viewer when sending destroys the displayed draft
2026-08-21 09:26:26 +02:00
Linus Rath 165ec6d321 Merge pull request #851 from paulhenry46/webauthn-fix
fix(plugins) : refactor PRF workflow into two for better compatibility
2026-08-20 23:43:12 +02: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 6a3bb3f459 feat: redesign the unread favicon badge as a compact keyline badge 2026-08-20 16:49:31 +02:00
Linus Rath 161e7ec92a i18n: restore key parity for pull-to-refresh and folder move_error strings 2026-08-20 16:33:33 +02:00
Linus Rath 37d85e5447 feat: fullscreen email view in the standard interface 2026-08-20 16:19:12 +02:00
Linus Rath fc88fc6d3b feat: drag a mail to a new browser tab to open it fullscreen 2026-08-20 15:55:44 +02:00
Linus Rath 65e70a8088 feat: address bar reflects the focused Pro tab + live deep-link delivery to mounted surfaces 2026-08-20 15:35:41 +02:00
Linus Rath d83c83b0a9 fix: drop the folder sidebar from fullscreen email 2026-08-20 15:11:25 +02:00
Linus Rath 3e5ed4f3c4 feat: folder tabs via drag & drop + sidebar in fullscreen email tabs 2026-08-20 15:06:45 +02:00
dealerweb dc0f5b179f fix: carry draft attachments in the Pro draft tab too
The #849 fix carries a re-opened draft's parts into the composer on the
classic path, but the Pro draft tab builds its own initialData and
still omitted email.attachments - editing a draft from a Pro tab kept
stripping its files on the next save. Pass the parts through; the
composer's existing initializer and remap handle the rest.
2026-08-20 14:45:17 +02:00
Linus Rath 485b93f777 fix: lengthen example SESSION_SECRET 2026-08-20 14:44:19 +02:00
Linus Rath b9fe961ae8 fix: session cookie not stored for relative JMAP server URLs (dev mock logouts) 2026-08-20 14:42:29 +02:00
Linus Rath 2d5748e6c9 fix: leaving the Pro interface returns to the surface in use and keeps the settings scroll position 2026-08-20 14:35:05 +02:00
Linus Rath 1c6cf60bbf fix: preserve settings scroll position when the Pro interface toggle remounts the settings surface 2026-08-20 14:29:16 +02:00
Linus Rath 46256cddba fix: position portalled popovers before first paint to stop layout flash on open 2026-08-20 14:23:05 +02:00
Linus Rath 54d7fb6fc9 fix: folder drag & drop can move folders into other parents #855 2026-08-20 14:21:41 +02:00
Linus Rath 50741d1131 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-08-20 14:00:42 +02:00
Linus Rath 9a6cc23876 fix: refresh TOTP-minted tokens with the default client id when no OAuth client is configured #873 2026-08-20 13:59:57 +02:00
Linus Rath 434005b170 fix: pass PDF bytes to pdf.js instead of fetching the blob: URL, which CSP connect-src blocks #871 2026-08-20 13:57:19 +02:00
Linus Rath a95267ba59 Merge pull request #853 from dev-hive-kazniisa/fix/jmap-keyword-patch-pointer
fix: keyword patches use an unescaped JSON Pointer, and a failed migration reports as a clean one
2026-08-20 13:53:39 +02:00
Linus Rath 3c299baa3e Merge pull request #831 from zisisvakras/fix/email-keyword-removal-uses-null
fix(jmap): remove keywords with null rather than false in Email/set
2026-08-20 13:37:42 +02:00
Linus Rath 5d2b937ae3 fix: keep attachments when re-opening a draft and destroy old draft versions only after a successful create/send #849 2026-08-20 13:25:25 +02:00
dealerweb 0fd83203cf fix: clear the viewer when sending destroys the displayed draft
Sending an edited draft destroys it server-side, but the viewer kept
rendering the deleted message after the composer closed: the mailbox
list refreshed (row gone, counter down) while selectedEmail still
pointed at the destroyed draft. Clear the selection like a delete does
- and return to the list on mobile - when the email on display is the
draft the compose session was opened from or its latest autosaved
successor (autosave replaces the draft under a fresh id on every save,
so matching the sent draftId alone would miss).
2026-08-20 13:12:06 +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
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