Commit Graph
65 Commits
Author SHA1 Message Date
Linus Rath 6e97ca574d fix: make switches that are off visible against the page 2026-10-28 01:50:56 +01:00
Linus Rath 7da326b86e fix: keep the server's path in the fallback CalDAV address 2026-10-26 15:15:58 +01:00
Linus Rath cf2c27157d Merge branch 'main' into feat/stalwart-1.0
# Conflicts:
#	components/contacts/contacts-app.tsx
#	components/email/__tests__/composer-discard.test.tsx
#	components/email/calendar-invitation-banner.tsx
#	components/email/email-composer.tsx
#	components/mail/mail-app.tsx
#	components/pro/pro-compose-tab-body.tsx
#	components/pro/pro-email-tab-body.tsx
#	components/ui/__tests__/avatar.test.tsx
#	components/ui/avatar.tsx
#	hooks/use-mailbox-drop.ts
#	lib/__tests__/calendar-alerts.test.ts
#	lib/__tests__/jmap-calendar-tasks.test.ts
#	lib/__tests__/jmap-send-threading.test.ts
#	lib/demo/demo-client.ts
#	lib/demo/fixtures/calendars.ts
#	lib/jmap/client-interface.ts
#	lib/jmap/client.ts
#	lib/web-push.ts
#	stores/__tests__/contact-store.test.ts
#	stores/__tests__/email-store-thread-key.test.ts
#	stores/calendar-store.ts
#	stores/contact-store.ts
#	stores/email-store.ts
2026-10-26 10:14:12 +01:00
Linus Rath 31f4a4b4b3 feat: show one row of themes with a show all button 2026-10-19 20:49:24 +02:00
Linus Rath 401d49471e feat: let settings links name a section and add a jump row to each page 2026-10-19 18:14:01 +02:00
Linus Rath a615f96876 refactor: regroup the settings sidebar into mail and apps with plugin pages under add-ons 2026-10-19 18:07:48 +02:00
Linus Rath d86aa1d12b refactor: group security into sign-in, other apps and devices, and encryption 2026-10-19 17:46:42 +02:00
Linus Rath 5b1d056eef feat: split the message list out of reading and fold content and senders into it 2026-10-19 17:40:52 +02:00
Linus Rath 988416aa3d feat: add an advanced switch that hides power-user settings until turned on 2026-10-19 17:20:38 +02:00
Linus Rath 58eff0e879 Merge pull request #1258 from HOLOEYE-JZ/feat/archive-feature-gate
feat: admin policy gate to turn off archiving #1257
2026-10-18 14:29:27 +02:00
Linus Rath 8e7245b26c feat: let the theme policy set and lock the color mode #1280 2026-10-18 13:46:49 +02:00
Linus Rath 481665b435 test: pick the role dropdown in the folder order test next to the counter menus #982 2026-10-17 19:18:17 +02:00
Linus Rath 1c3cc35a7c feat: set the counter per folder, e.g. keep the Drafts count with totals off #982 2026-10-17 19:18:16 +02:00
Jacob ZeitzandClaude Opus 5.5 99b0bced3b feat: admin policy gate to turn off archiving #1257
A new feature gate, archiveActionEnabled (default on), lets the admin turn the
Archive action off for installations that archive mail elsewhere, such as a
mail gateway. With it off, Bulwark offers no Archive anywhere: the viewer's
button and its overflow entry, the context menu for one or several messages,
Mountain View's selection bar, the hover and swipe actions, the E shortcut and
its help row, archiving after an export, and the archive mode setting (also in
the settings search). With it off nothing creates an Archive folder:
ensureArchiveMailbox returns null. Existing Archive folders stay ordinary
folders, moving mail into them by hand still works, and the plugin API is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:48:28 +02:00
Linus Rath 41a0716c50 Merge branch 'main' into feat/stalwart-1.0 2026-10-16 01:41:48 +02:00
Linus Rath 0e61705841 Merge pull request #1244 from lucletoffe/feat/calendar-sort-order
feat(calendar): order calendars by sortOrder and let users reorder them
2026-10-15 23:54:41 +02:00
lucletoffeandClaude Opus 5.5 de388a1d12 feat(calendar): order calendars by sortOrder and let users reorder them
Calendars were listed in the order the server returns them, which for
Stalwart is creation order, and the sortOrder property was never read.
Follow the JMAP Calendars rule instead: lower sortOrder first, equal
sortOrder by name (case- and accent-insensitive, numbers by value). The
store keeps the list sorted, so the sidebar, the calendar pickers and the
settings all show the same order; each account's calendars stay together.

Calendar settings gain move up / move down buttons, visible on touch
screens too, and drag and drop for the user's own calendars. A move
renumbers that account's calendars 1, 2, 3... and writes sortOrder only
where it changes. The renumbering also covers the account's calendars the
settings list does not show (iCal subscriptions), so they keep their
place relative to the others. Shared calendars are not offered for
reordering, their order is the owner's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 22:31:07 +02:00
lucletoffeandClaude Opus 5.5 8cf6783cdb feat(calendar): say whom a calendar is shared with
The sidebar showed the same people icon for any calendar with a share,
labelled "Share calendar", and the settings badge only gave a count, so
there was no way to tell from either who could see the calendar. A share
granted to a service account read like a share with other people.

Both now carry a hint listing the recipients ("Shared with
bot@example.com (Booking robot)"), resolved from the principals the
server lists. Until every recipient is known the hint falls back to the
count. Principals are fetched only when a calendar on screen is shared.

The per-client principal cache of the attendee availability check moves
to lib/jmap/principal-cache.ts so both use it. It no longer keeps an
empty or failed answer: the next caller asks again instead of seeing
no principals for the rest of the session.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 22:19:53 +02:00
Linus Rath b88232c4e8 fix: list a plugin with both a settings page and a settings section once, with the section on its page 2026-10-14 09:52:52 +02:00
Linus Rath 11b1ec2fa5 feat: show plugin setting labels in the user's language when the plugin translates them 2026-10-14 06:24:32 +02:00
Linus Rath d6eac01d1f feat: let plugins add a page of their own in Settings, under a Plugins group 2026-10-14 05:01:33 +02:00
Linus Rath c878e51fce fix: clear a calendar's default reminders with an empty list instead of null #1209 2026-10-13 15:56:10 +02:00
Linus Rath 1730d7bab4 feat: set default reminders for each calendar in calendar management #1209 2026-10-13 15:56:10 +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 7533fc516b fix: renew push for every connected account, not only the active one #1054
The background resync only touched the active client, so accounts
turned on from the all-accounts panel lapsed when the server's push
subscription expired after seven days. The resync now walks every
connected login in turn, active one first, and runs again once the
background logins finish connecting. Single-account behaviour is
unchanged.

The panel also lists which accounts failed to enable and why, instead
of only a count.
2026-10-10 19:24:43 +02:00
Linus Rath 02923fc1f1 Merge pull request #1054 from lucamzanon/feat/multi-account-push
feat: enable push notifications for several accounts at once
2026-10-10 19:24:10 +02:00
lucamzanon 9560a8b74f feat: optional encrypted backup of the account list
Users with several accounts can save the list of accounts in their browser
(address, server, label, colour) on the webmail server and bring it back on
another device. The list is encrypted in the browser with a password the user
chooses (PBKDF2-SHA-256, 600,000 iterations, AES-256-GCM, owner bound as
additional data); the server stores only the envelope, one file per owner
under SETTINGS_DATA_DIR/account-archives. No mailbox passwords are included.

Off by default behind the admin feature gate accountArchiveEnabled, pinned
off in Lite. Every method of /api/account-archive needs a session cookie of
the owner, matched without regard to case, whose credential is checked
against the mail server on each call; listing included.

Restored accounts this browser lacks are listed as awaiting sign-in: they
survive a reload, and opening one leads to the sign-in form with the address
filled in. Accounts already present get their archived label and colour.
2026-10-09 10:54:38 +02:00
Linus Rath 7f78d4837a fix: don't offer the auto-reply audience on a shared account #1152 2026-10-06 23:31:03 +02:00
Linus Rath cbfcdf3b69 Merge pull request #1152 from dealerweb/feat/rule-periods-out-of-office
feat: out of office forwarding and recipients, filter rule periods
2026-10-06 23:28:58 +02:00
Linus Rath 6e5c5390df Merge pull request #1162 from dealerweb/fix/focus-trap-rerender
fix: keep the focus in dialogs while the page behind renders again
2026-10-06 23:28:51 +02:00
Linus Rath 1e10aa9bd6 feat: move several folders at once from the folder settings #1173 2026-10-06 15:49:53 +02:00
dealerweb 0f7e6d0c16 fix: keep an open filter dialog through a reload in the background
A change to the Sieve script from another device or tab reloads the
filters, and the page showed its loading or error state instead of its
content. That took an open rule dialog or the Sieve editor with it,
together with whatever was typed there. While a dialog is open, the
page keeps what it has.
2026-10-02 12:42:40 +02:00
dealerweb 0c376dd0e4 fix: keep the focus in a dialog's field while the page behind renders again
useFocusTrap started over whenever its onEscape changed, and callers pass
a new function on every render. Each start moved the focus back to the
opener and then to the dialog's first element, so a field lost the focus
whenever the page behind rendered again: every 30 seconds through the
connection check, or when new mail updated the folder counts. The trap
now reads the latest onEscape from a ref and starts once per opening.

A view change in a dialog (a list giving way to a form) can remove the
element that had the focus. The restart used to pull it back by
accident; the trap now does that on purpose, and only when the focused
element was removed.
2026-10-02 12:42:40 +02:00
dealerweb f5fb7f9db9 feat: out of office forwarding and recipients, filter rule periods
Filter rules can be limited to a period: the rule dialog takes a start
and an end on the user's clock, the rule list shows whether a rule is
planned, active or expired, and the script tests the moment of
delivery with RFC 5260 currentdate in UTC. A rule is never written
without its period.

The vacation card becomes an out of office page: the period on top,
then the automatic reply and forwarding, each with its own switch.
Forwarding runs with or without the auto-reply, within the period,
never for spam, and keeps a copy here or not. Against the server's
redirect limit it counts as a rule ahead of all others, one that stops
the message unless it keeps a copy. The auto-reply can go to
internal or external senders only, told apart by the envelope sender
against the account's own domains. Both run from the filters script
that includes Stalwart's vacation script, which is put back in charge
when the server's own script takes over.

A script that uses any of this says metadata version 2, which older
builds leave alone instead of writing it back without these fields.
2026-10-02 11:29:32 +02:00
Linus Rath d8f10f4fd6 Merge pull request #1148 from lucamzanon/fix/push-settings-refresh
fix(settings): check push again when the user comes back to the tab
2026-10-02 10:30:43 +02:00
Linus Rath 15839afec2 Merge pull request #1154 from dealerweb/feat/filter-all-messages
feat: a filter condition that matches every message
2026-10-02 10:30:42 +02:00
dealerweb 6da2bf9a43 fix: count the forwards one message can collect against the redirect limit
The rule editor added up the forwards of all enabled rules against the
server's redirect limit, which applies per message. Rules run in order,
and a rule that stops ends the script for its messages, so a rule that
forwards and stops locked forwarding in every other rule, although no
message can reach both.

Count the most forwards one message can collect instead, and warn above
the rule list when the order, a switch or a drag lets one message
collect more than the limit.

Builds on the stop fix: without it, "Stop processing" after "Delete
silently" or "Reject" writes no stop.
2026-10-01 16:33:51 +02:00
dealerweb c55b9f4929 feat: a filter condition that matches every message
The rule editor had no way to say "every message that arrives"; the
workaround was a condition like "From contains @", which only works if
you know it. "All messages" is now a condition of its own, at the end of
the field list. It takes no comparator and no value, writes Sieve's
`true` test, and combines with other conditions as any condition does.
The rule list shows it on its own. A rule with it is never applied to
the messages already in a folder: it would act on all of them.
2026-10-01 14:58:00 +02:00
lucamzanon ce3b12aa21 fix(settings): check push again when the user comes back to the tab
The notification settings read the push state once, when they opened.
Push turned off meanwhile - in the browser's site settings, or from
another tab - still showed as on until the page was reloaded. The state
is now read again when the window regains focus or the tab becomes
visible, unless an enable or disable is in progress or has just failed.
A failed check reads as off instead of leaving the panel unchanged.
2026-10-01 12:43:40 +02:00
Linus Rath 410d6b3532 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-10-01 11:52:29 +02:00
Linus Rath 4f48af6307 fix: spam siege restyle 2026-10-01 10:29:20 +02:00
lucamzanon 405c0e656e fix(push): show each login's real push state in the all-accounts panel
Push state is stored under the JMAP account id the server assigned
(enableWebPush keys it on client.getAccountId()), but the all-accounts
panel looked it up under the login's own id, so every account read
"notifications off" even right after enabling them. Read it back
under the JMAP account id.
2026-10-01 07:36:44 +02:00
dealerweb 9130ec7e14 fix: wrap a filter condition's values in the expanded rule view
The values of a multi-value condition were glued together as one
unbreakable run - the space around each "or" was a margin, not a
character the line could break at - so a rule with several addresses ran
out of its card. Each value is now an item of its own: they wrap between
each other under the first value, with "or" leading the next line and
still inside the condition's chip, apart from the "and"/"or" between
conditions. A single value too long for any line, in a condition or an
action, breaks inside itself instead of overflowing.
2026-09-30 23:40:13 +02:00
Linus Rath e4ef2fa3d6 fix: keep an abandoned pairing re-auth from breaking the next sign-in
The Link Mobile App step-up marked the tab before asking the server to
start the provider round trip. When that request failed, or the user
backed out at the provider, the mark stayed, and the next sign-in's
callback in that tab went to the pairing route and failed. The mark is
now set only once the round trip starts, holds that round trip's state,
and the callback drops one that does not match.
2026-09-27 04:56:48 +02:00
Linus Rath 377c283d86 fix: say up front when the mobile app cannot use an http mail server
The app refuses a JMAP server over plain http (loopback aside), yet the
desktop issued a code, reported "Signed in on your phone" once it was
redeemed, and the phone failed. Linking now stops before the step-up
with an explanation.
2026-09-27 04:53:49 +02:00
Linus Rath 96e0e6b036 fix: keep the pairing QR on screen while the Security tab checks the server 2026-09-27 04:18:09 +02:00
Linus Rath 1446fd39dc fix: link the mobile app from every account with a sign-in of its own
"Link Mobile App" only appeared for OAuth sessions, its step-up could
only go through the identity provider (which failed without an OAuth
client and bounced password+TOTP users to the provider's login page),
and it handed the phone the desktop's own refresh token. Revoking that
token on sign-out, or a rotating provider, signed the phone out too; a
confidential client stopped the phone at its first token expiry; and an
identity provider on another host was refused by the app.

The step-up now mints the phone's own grant:

- Password and password+TOTP accounts confirm the current password (and
  code) in the dialog. The server uses it once to get an OAuth grant
  from Stalwart's structured login for the app's own client, retries
  with the webmail's client when the server insists on a registered one,
  and falls back to an app password ("Bulwark mobile (paired <date>)")
  or, on servers that offer neither, the verified password. Failed
  attempts are limited per account so the webmail's address cannot be
  banned by guessing through it.
- SSO accounts keep the provider round trip, now with `offline_access`
  where offered; its fresh tokens become the phone's grant instead of
  being thrown away, and must belong to the account being paired.
- The account and server always come from the slot's verified session
  context, never from the request.

Grants live 5 minutes server-side, codes 2 minutes, both single use; the
store is pinned on `globalThis`. Redeem answers `invalid_code`,
`expired_code` or `used_code`, and the desktop polls a separate status
id to replace the QR with "Signed in on your phone". A phone whose
client is confidential, or whose provider is on another host, renews
through `/api/auth/pair/token` with a sealed refresh token, so current
app builds work unchanged. Signing out discards a pending grant but no
longer affects a paired phone.

The dialog shows for every account (not in Lite), with a copyable
sign-in link, a TOTP field when needed, and the app's own "Scan a
sign-in code" wording.
2026-09-27 04:16:18 +02:00
Linus Rath 5a553a315b fix: report refused read changes and re-register push at its limit
Follow-ups to the review of the push, keyword and identity work:

- A send that names no sender, like the List-Unsubscribe mailto: of the
  unified view, fell back to the first identity in server order, which
  on Stalwart 1.0 is a group's. It now takes the login's own identity,
  with addresses compared regardless of case.
- Re-register creates the replacement subscription before it destroys
  the old one, so at Stalwart's limit (15 per account by default) it
  failed with `overQuota`. It now frees the old record's place and
  tries once more.
- On 1.0 the push settings note goes by the emailpush capability alone
  and ignores refusals remembered under 0.16. On 0.16 a group the user
  belongs to is recognised by its identity, so a member without
  calendars, contacts or files keeps the group's junk filter and is no
  longer told the group sends no notifications.
- Plugin imports can no longer set Stalwart 1.0's attachment keywords,
  and `keywords.discover` leaves them out.
- Batch mark-as-read showed messages the server refused as read. It now
  updates only what was written and reports the rest, and thread
  expansion and category-tab moves keep what a partly refused batch
  did write.
- Refused read and star changes from shortcuts, the list, the viewer
  and Pro tabs show "Failed to update email" instead of an unhandled
  rejection. The automatic mark-as-read on opening stays quiet.
- A tag whose id predates the 128-byte limit can have its colour
  changed again.
- The state-regression guard notes that a multi-node 1.0 cluster can
  trip it harmlessly, at the cost of one extra full fetch.
2026-09-25 21:57:11 +02:00
Linus Rath f7deb92d16 fix: explain a closed directory and refused free/busy in the right words
With directory queries off on Stalwart 1.0, the free/busy list under the
attendees disappeared without a word. The share dialog explained every
`forbidden` from Principal/query with the 1.0 text: "only your groups
and people who have shared with you are listed", and a hint at
Stalwart's sharing settings. On 0.16 that refusal only means the role
lacks the permission, Principal/get then lists everyone, and the setting
has no effect over JMAP.

- When the server refuses free/busy for the user (`mayGetAvailability`
  false on the user's own principal, or a lookup refused with
  `forbidden`), one muted line says "Free/busy isn't available on this
  server". The block stays empty only when the server offers no
  free/busy at all.
- The share dialog keeps the 1.0 wording for Stalwart 1.0's own refusal
  ("The administrator has disabled directory queries.") and otherwise
  says "Your server doesn't let you browse its user directory.", still
  listing whoever Principal/get returned.
2026-09-25 20:07:46 +02:00
Linus Rath 9041cbcd08 fix: keep iCal subscriptions on refresh, rename calendars of any colour
- send a calendar's colour only when the user changed it: Stalwart 1.0
  accepts only #rgb, #rrggbb or a CSS name, so renaming a calendar that
  another client coloured with an 8-digit hex (Apple Calendar) failed
- "Refresh cached data" kept nothing of the calendar store's persisted
  state, so it deleted the user's iCal subscriptions, which exist only
  there; keep them and drop the rest
- show a notice with a retry when the task list fails to load; it keeps
  the tasks it had
- treat a method-level error on creating a task as a failure with its
  JMAP type instead of "no id returned"
2026-09-25 19:41:00 +02:00