Commit Graph
1430 Commits
Author SHA1 Message Date
Stefan Hildebrandt 88a809bd69 test(integration): verify unread/starred cross views keep the acted-on row
Adds 11-cross-view-retain.spec.ts: with the Unread cross view open, reading a
mail keeps it in the list (status + unread counter update only, no removal);
with the Starred cross view open, un-starring keeps it. Both force the
focus/push refresh that previously dropped the row and assert it survives.

Supporting test hooks:
- thread-list-item: expose data-starred on the row.
- email-context-menu: testId ctx-star / ctx-unstar on the star item.
- integration helpers: expectEmailStarred (app.ts), setFlagged (jmap.ts).
- integration policy.json: enable crossStarredViewEnabled.

Verified: 08-shared-moves-style run of 11-cross-view-retain → 2/2 passed
against the dockerized stack.
2026-08-09 22:17:58 +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 9d829d243c Merge pull request #764 from paulhenry46/encryption-at-rest-fix
fix: per-account isolation encryptionAtRest settings
2026-08-08 14:08:57 +02:00
Paulhenry Saux a3c06e89f2 fix: per-account isolation encryptionAtRest 2026-08-08 11:04:54 +02:00
Linus Rath a066108d35 chore: update version to 1.8.1 1.8.1 2026-08-07 01:56:25 +02:00
Linus Rath 7cb534348d Merge pull request #685 from paulhenry46/contact-cryptokeys-slot
feat: add contact-cryptokeys plugin UI slot
2026-08-07 01:43:16 +02:00
Linus Rath b50f71fa5c chore: update version to 1.8.0 1.8.0 2026-08-06 19:32:26 +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 ae4996f28b i18n: restore key parity and the en send_timeout string 2026-08-06 18:59:58 +02:00
Linus Rath 706c6c23fd fix: send via the identity's own account client so DKIM matches the From domain #461 2026-08-06 18:55:08 +02:00
Linus Rath 69c8c041f9 feat: detect and import unrecognized JMAP keywords as tags #658 2026-08-06 18:44:14 +02:00
Linus Rath 1b10e027bd fix: keep inline images when replying to octet-stream cid parts #543 2026-08-06 18:41:01 +02:00
Linus Rath cfe2af4d8a fix: stop re-adding the organizer to the attendee list on every save #731 2026-08-06 18:29:34 +02:00
Linus Rath 11b6bdd8a6 fix: sync PWA title bar colour with the active theme #671 2026-08-06 18:23:21 +02:00
Linus Rath d55aba3890 fix: honor configured PWA theme color in desktop title bar #671 2026-08-06 17:40:41 +02:00
Linus Rath 23c4ab87da feat: confirm sending without a subject instead of blocking it #684 2026-08-06 17:31:50 +02:00
Linus Rath e2da05c130 feat: deep links for mail, calendar, contacts, files, settings and improve screen reader support
#733 #722 #721  #720  #719
2026-08-06 16:47:20 +02:00
Linus Rath b3f5f51efa fix: stop minting duplicate "Trusted Senders" address books #730 2026-08-06 15:07:54 +02:00
Linus Rath 6f293bbf04 Merge pull request #729 from MrDeik/agent/push-notification-onboarding
feat: add background notification onboarding
2026-08-06 14:47:31 +02:00
Linus Rath e9c4929913 fix: address review feedback on push onboarding
- track session-only dismissal per account instead of a single account id
- label the close button with a dedicated dismiss aria string in all locales
- read PWA install prompt visibility from a shared flag and subscribe in a
  layout effect so an early visibility event cannot be missed
- restore real timers after the push onboarding tests
2026-08-06 14:46:32 +02:00
Linus Rath d26101417c Merge pull request #751 from romanvru/feat/plugin-attachment-offload
Plugin API: attachment offload from onBeforeBlobUpload, binary http.post, blob-read for upfiles.get
2026-08-06 14:09:49 +02:00
Linus Rath f056db5a05 fix(plugins): make attachment offload workable and keep the write gate closed
Review follow-up to #751.

The offload path could not actually run: onBeforeBlobUpload goes through
HookBus.transform, which capped every handler at the 5s default. A handler
that pushes the file to an external store blows past that, and on timeout
the error is swallowed, the original file id is kept, and the composer
attaches the big file anyway - silently - while three such timeouts in a
minute auto-disable the plugin. Buses can now carry their own per-handler
budget and this one gets 120s. The same ceiling existed inside the sandbox,
where http.post/http.fetch were bounded by the 30s default API timeout;
those two are transfers rather than round-trips and now get 120s as well.

PRIVILEGED_ONLY_METHODS listed 'upfiles.set', which matches no dispatched
method - the dispatcher calls it 'upfiles.save'. So the write was never
tier-gated despite the comment saying it was, and an untrusted plugin with
email:blob-write could replace the bytes of a file the user was about to
send. Fixed the name. Relaxing the read while that stayed open was the
wrong order.

Moving upfiles.get to email:blob-read is kept, but it is a wider grant than
the PR described: onBeforeBlobUpload hands the file id to every registered
handler, so the id is not a secret and any untrusted plugin holding
email:blob-read can read every file the user attaches. The consent copy now
says that; email:blob-write's copy was describing the read and now describes
the replace it actually gates.

Also in the composer offload path:
- sanitize the handler's html before it reaches the document and the sent
  message, and insert it through the editor at the caret rather than
  concatenating it after the signature and quoted block
- honour an abort that lands while the handler is offloading
- delete the right staged file when the handler re-saved it under a new id
  via upfiles.save, which otherwise stranded a copy in IndexedDB forever
2026-08-06 14:08:07 +02:00
Roman Varlakov 94c7a454cc feat(plugins): move upfiles.get behind email:blob-read, off the privileged tier
upfiles.get is a read, but sat behind the email:blob-write permission and
the privileged tier, which requires crypto:full plus a signed managed
bundle. That put a simple attachment-offload plugin in the same trust
bucket as crypto plugins that read stored message bytes.

The method only returns a file the user just attached in this session,
looked up by the unguessable UUID the composer itself passes to the
onBeforeBlobUpload hook. It cannot reach server-side message blobs; those
stay behind jmap.fetchBlob on the privileged tier. So gate it behind
email:blob-read (consent copy already covers attachment bytes) and drop
the tier requirement.

Note: the privileged set also lists 'upfiles.set', but the dispatched
method is named 'upfiles.save', so that entry never matched; left as-is
here, flagged in the PR.
2026-08-06 17:25:52 +06:00
Roman Varlakov 600ab02034 feat(plugins): binary Blob/File bodies for api.http.post
api.http.post(path, body, options) now accepts a Blob/File body and sends
it as-is to the same-origin route instead of JSON.stringify-ing it into
'{}'. The plugin may only supply Content-Type and X-Plugin-* metadata
headers; anything else is refused, and the host attaches its credential
headers after the plugin's, so they can never be overridden. Non-Blob
bodies keep the stock JSON behaviour. The sandbox facade previously
dropped the third argument, so the options never reached the host.
2026-08-06 17:25:42 +06:00
Roman Varlakov 656a9c9c3b feat(plugins): let onBeforeBlobUpload offload an attachment
A handler can now return an ExternalAttachmentResult ({externalAttachment:
true, html, text}) instead of a file id. The composer then deletes the
staged file, removes the attachment and appends the replacement content
to the body, so a plugin can push a large file to an external store and
leave a download link behind. Returning a string (or nothing) keeps the
stock upload path byte-for-byte.

Also adds plainTextMode to the upload callback deps, which the new branch
reads to pick between the html and text replacement.
2026-08-06 17:25:27 +06:00
Linus Rath 5ed25dda98 Merge pull request #743 from paulhenry46/plugin-api-crypto
feat: new crypto methods for plugin API
2026-08-06 12:25:01 +02:00
Paulhenry Saux 1979c27d9e fix: use correct method 'crypto.getPublicKeys' in crypto plugin API 2026-08-06 07:36:18 +02:00
Linus Rath d68ebb55c0 Merge pull request #746 from bulwarkmail/copilot/fix-comments-in-review-thread
Restrict shared-account capability fallback to explicit non-personal accounts
2026-08-06 00:54:23 +02:00
Linus Rath 4b2970ba80 Merge pull request #706 from polastre/check-account-capabilities
fix(jmap): hide Contacts and Calendars when account lacks capability
2026-08-06 00:54:13 +02:00
copilot-swe-agent[bot]andrathlinus 6f9fc53502 fix: require explicit shared account fallback for contacts/calendars
Co-authored-by: rathlinus <139418639+rathlinus@users.noreply.github.com>
2026-08-05 22:41:26 +00:00
copilot-swe-agent[bot] aa2bc9985c Initial plan 2026-08-05 22:38:39 +00:00
Linus Rath ab1f89d633 Merge pull request #740 from koolninad/feat/mail-list-refresh-button
feat: add a refresh button to the mail list toolbar
2026-08-06 00:33:40 +02:00
Linus Rath 57d802db85 Merge pull request #704 from guisea/fix/sent-attachment-list-hidden-when-unwrapped
fix: keep sent message/rfc822 attachment visible after inline unwrapping
2026-08-06 00:30:23 +02:00
Linus Rath 87a65e7dd9 Merge pull request #742 from paulhenry46/stalwart-public-keys-encryption-at-rest
feat: manage public keys and at rest encryption
2026-08-06 00:29:02 +02:00
Paulhenry Saux d573cda920 feat: add new crypto API plugin methods 2026-08-05 21:55:50 +02:00
Paulhenry Saux 3b519c3b3b feat: manage public keys and at rest encryption 2026-08-05 21:46:42 +02:00
Ninad Chandorkar b797ea88b8 feat: add a refresh button to the mail list toolbar
The only ways to re-fetch mail are the browser refresh gestures (F5,
Ctrl/Cmd+R, pull-to-refresh) that useRefreshGesture intercepts. On a
desktop that is discoverable enough, but there is no visible control,
and users who reach for one find nothing.

Extract the gesture's inline onRefresh into a shared handleManualRefresh
with a spinner state, then reuse it for a refresh button next to the
filter control in the mail-list toolbar. The icon spins while the fetch
is in flight and the button is disabled until it settles, so repeated
clicks can't stack requests.

Uses the existing common.refresh translation key, so no new strings.

Also guard the login submit with rememberMeEnabled: the checkbox is only
rendered when the server has a SESSION_SECRET, but the value was passed
through unconditionally, asking for a credential cookie the server will
not write.
2026-08-05 23:37:36 +05:30
Linus Rath e27f17b796 Merge pull request #736 from dealerweb/fix/calendar-parse-debug-log
fix: route the calendar parse dump through the debug logger
2026-08-05 17:20:32 +02:00
Linus Rath da17464e05 Merge pull request #737 from dealerweb/fix/internal-mailto-links
fix: open address links in the built-in composer
2026-08-05 17:20:18 +02:00
Linus Rath 151af5ced0 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-08-05 17:19:40 +02:00
Linus Rath 0fe484efda fix: time out stalled JMAP requests so sends can't hang forever #702 2026-08-05 17:19:10 +02:00
dealerweb 04996a3af1 fix: open address links in the built-in composer
Clicking an address in the app's own chrome - the recipient card, the
contact sidebar, the contact details - handed a mailto: URL to the OS
mail handler. In a webmail client that goes nowhere: the browser asks
which application should open the link, and the message is composed
somewhere else, if at all. Only users who registered Bulwark itself as
their system mailto handler got a usable result, and even then via a
detour through the OS into a new tab.

A new MailtoLink component sends the click to the built-in composer
instead. It stays a real anchor with an href, so "Copy email address",
middle click and assistive technology keep working; only the plain
left click is redirected, and only when a listener takes it - otherwise
the browser's own behaviour still applies. Modified clicks are left
alone.

The request reaches the mail page through a window event, the way
`bulwark:rate-limit-blocked` already does, rather than threading a
callback through three component levels and five call sites. It goes to
openMailtoDraft rather than the protocol path: an in-app click carries
its own context - the account being read - so the account picker that an
external mailto: needs would only get in the way here.

Message content is deliberately untouched. Plain text bodies render into
the main document rather than the sandboxed frame and their sanitizer
permits mailto:, so a document-wide interceptor would have swept up
untrusted links too; a component cannot be reached from message HTML at
all.
2026-08-05 14:42:24 +02:00
dealerweb 264a8493fb fix: route the calendar parse dump through the debug logger
Opening a message with a calendar invitation printed the whole parsed
event to the console on every render - title, description, attendees and
location included - through a leftover `[PARSE DEBUG]` console.log.

The surrounding file already logs via debug.log('calendar', ...) in 22
places, which honours the debugMode setting and its category filters, so
this one line was the outlier. It also ran JSON.stringify with
indentation unconditionally, even though nothing was listening.

Passing the object to debug.log instead leaves the formatting to the
logger, so nothing is serialised unless the calendar debug category is
actually switched on.
2026-08-05 14:40:21 +02:00
Eduard Polonskyi e036e10004 fix: sequence PWA and push onboarding 2026-08-03 15:31:17 +02:00
Eduard Polonskyi 573fab8b83 feat: add background notification onboarding 2026-08-03 03:23:08 +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
Linus Rath 15982c2468 Merge pull request #708 from paulhenry46/user-api-plugin
feat(plugins): add 2 new methods : getAccounts and getIdentities
2026-08-01 23:06:53 +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
Paulhenry Saux 6698ec8456 Merge branch 'bulwarkmail:main' into user-api-plugin 2026-08-01 20:24:08 +02:00