Commit Graph
76 Commits
Author SHA1 Message Date
honzupandLinus Rath 9072bf8470 fix: keep sidebar tag counts in step with read/unread changes
Marking mail as read left the sidebar's tag unread counts untouched — the
folder counts cleared, but a tag went on showing "47 unread" in bold until
the page was reloaded.

tagCounts is fetched from the server (Email/query per $label keyword) rather
than derived from state, and no read/unread mutation refreshed or adjusted
it. The per-mailbox unreadEmails counters were kept current by a local delta;
tags simply had no equivalent.

Add applyTagCountReadDelta alongside the existing mailbox-counter helpers and
apply it wherever the affected emails are known locally: markAsRead,
batchMarkAsRead, and setEmailKeywordsLocal. Only a genuine $seen flip moves a
count, so re-marking a read email as read cannot drift it, and unread is
clamped at zero. A tag's total is never touched by a read-state change.

markMailboxAsRead is the exception and refetches instead: it is a server-side
bulk operation over an entire mailbox, so it also marks emails that were never
loaded into state.emails, and a local delta would leave the counts high.
2026-07-13 21:19:08 +02:00
Paulhenry SauxandLinus Rath 622adc34de feat: add onEmailsFetched and onSearchResults hook + new JMAP method getSomeEmails 2026-07-13 18:18:57 +02:00
dealerwebandLinus Rath d384c3b553 Feature: pin emails to the top of the folder list
Outlook-Web-style pinning: a context-menu Pin/Unpin action stores a
$pinned keyword on the message (plain IMAP-compatible flag, survives
other clients), and pinned mails stay at the top of the folder list
regardless of age, marked with a pin icon.

Ordering is done server-side via the hasKeyword sort comparator
(RFC 8621), applied consistently to the folder fetch, pagination and
the push-refresh so page windows stay stable. The client-side safety
sort in getEmails mirrors it, and sortThreadGroups keeps threads
containing a pinned mail on top so the client-side thread grouping
does not undo the order.

The new-mail notification in refreshCurrentMailbox now checks the
first non-pinned entry: with pinned mails on top, the newest mail is
no longer at index 0 and arrivals would never have notified.

The toggle reuses the color-tag pathway (routed keyword write for
unified views, in-place local patch), then refetches the first page
so the mail floats or sinks immediately. Search and unified views
keep their existing order.

Pin/Unpin strings are added to all 21 locales.
2026-07-06 13:36:25 +02:00
dealerwebandLinus Rath 06ddda688d Fix: spam actions left folder counters and the open message stale
Marking mail as spam (or not spam) updated the email list but nothing
else, unlike delete/move which patch the folder counters optimistically.
Without a connected JMAP push the sidebar badges simply never moved, and
"not spam" additionally left the reading pane stuck on the message that
had just left the folder.

markAsSpam now mirrors moveToMailbox: source folder counts down, junk
counts up (honoring that trash-and-read delivers the mail to junk
already read). batchMarkAsSpam and batchUndoSpam mirror the batch move
pattern the same way.

undoSpam advances the selection to the next message like markAsSpam
already did, and refreshes the mailbox list instead of patching counts:
in the undo-toast path the email is no longer in the list, so its unread
state is unknown and an optimistic patch is not possible.
2026-07-06 11:04:20 +02:00
Linus Rath cd247e9c47 Merge remote-tracking branch 'origin/main' into feat/all-mail-cross-account-views
# Conflicts:
#	stores/settings-store.ts
2026-06-24 16:05:00 +02:00
Stefan HildebrandtandLinus Rath 751f3c1685 feat: per-account All Mail folder selection
Replaces the global allMailFolderIds (string[] | null) with a per-account
Record<accountId, string[]>, so each account chooses which of its own folders
the "All Mail" view merges. A missing entry = "not configured" (defaults to
every no-role folder); an explicit [] = "no folders".

- settings-store: type/default -> Record (default {}); persist version 4 -> 5,
  migration drops the legacy global list (the active account isn't known at
  migrate time); onRehydrate + importSettings coerce/ignore any non-record
  (legacy global string[] | null) shape. isPlainRecord() guard.
- email-store.resolveAllMailJmapIds: reads the entry for the account the view is
  scoped to (viewingAccountId ?? activeAccountId); undefined -> all no-role,
  [] -> none.
- layout-settings: read/write the active account's entry; when more than one
  account is logged in, an italic hint names the account the selection applies
  to (settings.appearance.all_mail.account_hint, 19 locales; de/ro translated).
- Test: stores/__tests__/settings-store-all-mail.test.ts (per-account
  independence, explicit-empty vs not-configured, importSettings legacy guard).
2026-06-24 15:52:09 +02:00
Stefan Hildebrandt fa3c57467b fix: route all counter updates to the email's own account in aggregate views
Extend the counter-routing fix beyond markAsRead to every optimistic mailbox
counter update, so a different account's email never adjusts the active
account's folder counters (JMAP ids can collide across accounts).

- Add applyBatchMailboxCounterUpdate() + applyDeleteCounters() and apply the
  per-account routing to: deleteEmail (trash + permanent), moveToMailbox,
  moveEmailsToMailbox, batchMarkAsRead, batchDelete, and markThreadAsRead.
- markAsSpam/batchMarkAsSpam/batchMoveToMailbox don't touch counters (rely on
  refresh) and folder-level ops (rename/empty/markMailboxAsRead) are already
  account-scoped — left as-is.
- Test: batchMarkAsRead adjusts each account's counter in its own list.
2026-06-23 19:16:23 +02:00
Stefan Hildebrandt befee332d2 fix: route unread-counter update to the email's own account in aggregate views
In a cross-account view, marking a second account's email read/unread updated
the *active* account's folder counter instead of the email's. Two causes: the
optimistic counter update only touched `state.mailboxes` (the active account),
and JMAP mailbox ids can collide across accounts so the id match hit the wrong
folder.

Add applyMailboxCounterUpdate(): route the counter delta to the list that holds
the email's folders — the active account's `mailboxes` (incl. its shared
folders) for active-account/shared emails, otherwise that account's
`accountMailboxes[sourceClientAccountId]` entry. Use it in markAsRead.

Regression test: a 2nd-account email with a colliding inbox id decrements that
account's counter and leaves the active account's untouched.
2026-06-23 19:16:22 +02:00
Stefan Hildebrandt a29c33b50a feat: cross-account "All accounts" views + full group/shared-account support
Add cross-account aggregate mail views and make group/shared (delegated)
accounts first-class in every aggregate view. (The unified mailbox, the "All
Mail" view, and "include group inboxes" already exist on main; this branch adds
the cross-account views and the shared-account correctness work.)

New views (admin-gated + per-user toggle, nested under Unified Mailbox):
- Cross-account "All accounts": All unread / All starred / All mail across every
  connected account, including shared/group folders. Each list labels the source
  folder of every message.

Source reference on aggregated emails (the core of the shared-account work):
- Replace the overloaded `accountId` with two explicit, always-set fields:
  `sourceClientAccountId` (the login the mail is reachable through) and
  `sourceAccountId` (the owning JMAP account). `accountId` stays display-only.
- Resolution is branch-free everywhere: pick the client by sourceClientAccountId,
  pass sourceAccountId as the JMAP accountId (no-op for personal), read the
  owner's mailbox list cached by JMAP id. No capability scan.

Shared/group-account correctness across all aggregate views:
- Route open (click + auto-fetch), thread/conversation open + reply-refresh,
  mark read, star, move, delete (account-scoped trash), archive (owner-routed
  createMailbox / fetchAccountMailboxes), and spam + undo via the source ref.
- Add accountId params to toggleStar / batchMarkAsRead / batchDeleteEmails /
  createMailbox where missing.
- Fix local unread/total counter math for shared folders via emailInMailbox()
  (matches namespaced shared ids and bare own ids).
- Keep the unified/cross virtual selection on background mailbox refresh (no
  jump back to inbox after deleting in All Drafts/Junk).

Junk UX:
- In "All Junk" the spam action becomes "not spam" in the viewer, context menu,
  and list hover icons; undo routes shared mail back to its own inbox.

Admin:
- Policy gates crossUnread/Starred/AllViewEnabled, each noting the matching
  per-user toggle (allMailViewEnabled clarified too).

i18n / docs / tests:
- locales (19): cross-view labels + descriptions and hover not_spam, translated
  in all shipped languages.
- FEATURES.md + README.md document the new views and group-account support.
- Tests for shared-account routing (single + batch + undoSpam), decoration, and
  unified-selection preservation.
2026-06-23 19:16:22 +02:00
Max HaoandLinus Rath fd700f412e fix: fix inconsistent behavior with threading email messages in the inbox/folders 2026-06-15 23:00:30 +02:00
Linus Rath c51c3655d5 feat: add "All Mail" view 2026-06-15 18:42:23 +02:00
Linus Rath 569dde9985 fix: preserve folder list when mailbox refetch hits concurrent-request limit 2026-06-11 19:10:06 +02:00
Linus Rath 44781f002c fix: empty Trash for shared and group folders #387 2026-06-03 20:18:02 +02:00
Linus Rath 152ec99262 feat: add Elastic built-in theme 2026-06-01 23:28:15 +02:00
Linus Rath c78dbee60b fix: move mail from shared group inbox to personal inbox #375 2026-06-01 17:39:28 +02:00
dealerwebandLinus Rath bebb394f54 Feature: read receipts (MDN, RFC 8098)
Bulwark had no read-receipt support (JMAP/Stalwart have no native MDN).
End-to-end, client-side, in three parts:

- Request (compose): a toolbar toggle (MailCheck, green when on) sets
  Disposition-Notification-To on the outgoing message via the JMAP
  "header:<name>:asText" create property. Threaded composer -> page ->
  email-store -> client.sendEmail. Default from requestReadReceiptDefault.

- Detect (viewer): reads Disposition-Notification-To case-insensitively from
  the parsed headers and shows a banner (green Send / red Ignore) in the
  unified notification bar. Hidden in Sent/Drafts/Trash/Junk and once handled.
  message/disposition-notification + message/delivery-status report parts are
  filtered out of the attachment list.

- Respond (MDN): lib/mdn.ts builds an RFC 8098 multipart/report (text/plain +
  message/disposition-notification, UTF-8/base64, localized subject + body).
  client.sendReadReceipt uploads the blob, imports it into Sent via
  Email/import, then submits with an explicit envelope. Both Send and Ignore
  set the $MDNSent keyword (RFC 3503) so no client re-prompts. Behaviour
  configurable: ask / always / never.

New: lib/mdn.ts, read-receipt-banner.tsx. Settings (requestReadReceiptDefault,
readReceiptResponse) + UI. All 17 locales.
2026-05-30 15:58:39 +02:00
31e96d6a46 Feat: Scheduled send and send delay #322
* ADD DOC

* Scheduld Send

* add new shortcuts

* fix

* fix

* fix bugs

* rework

* fix draft duplicating

* fix err

* some fixes

* fixes from review

* fixes from review

* fixes from review

* disable password managers for recipients

* fix email store lazy load

* add translations

* fix styling

* fixes

---------

Co-authored-by: Linus Rath <139418639+rathlinus@users.noreply.github.com>
2026-05-28 18:46:49 +02:00
Linus Rath 3a8daf8bff feat: allow drag-and-drop into shared mailboxes 2026-05-25 18:53:54 +02:00
Linus Rath 537707d9ed feat: include group inboxes in unified mailbox view #328 2026-05-23 16:01:22 +02:00
Linus Rath acc90eb455 feat: add "Move to Trash and mark as read" delete action #323 2026-05-22 19:08:02 +02:00
Linus Rath fc5f6f43d6 feat: expose PWA, app identity, and extension directory keys in JSON config #312 2026-05-21 23:35:58 +02:00
Linus Rath 280f5bc675 feat: support cross-account email moves in Pro shell 2026-05-21 17:22:31 +02:00
Linus Rath 1756f5ac1c feat: enable search in unified mailbox in pro mode 2026-05-21 17:12:06 +02:00
Linus Rath 426d344aa8 feat: multi-account mail sidebar and client routing for Pro shell 2026-05-21 16:52:03 +02:00
Linus Rath ed90e096b5 feat: add per-account mailbox cache for Pro shell data layer 2026-05-21 16:11:26 +02:00
Linus Rath b3dc2e32b8 feat: implement prefetching of initial email data 2026-05-11 15:17:30 +02:00
Augustin MarcinandLinus Rath b0640c9ecc feat(compose): From override + catch-all auto-reply (fixes #246)
Adds an Override toggle in the composer's From row. When enabled, name
and address become free-text inputs. Mail is still submitted through the
selected identity, but the outgoing message's From: header — and the
SMTP envelope MAIL FROM when different — is set from the override.

The existing "Auto-select Reply Address" setting is extended: if the
incoming message was addressed to an alias on a domain that matches one
of your identities but isn't itself an identity (classic domain catch-
all), it now auto-enables Override and pre-fills the alias. Quick reply
honors the same resolution. The setting is relabeled to reflect the
broader behavior.

JMAP: client.sendEmail gains an optional envelopeMailFrom; when set, the
EmailSubmission includes an explicit envelope with that mailFrom and the
to/cc/bcc as rcptTo so header-From and envelope can diverge (JMAP §7.3).

S/MIME: override is incompatible with sign/encrypt and is refused with a
clear error — signing a different visible From from the identity's
certificate Subject would produce messages clients reject.

Tests: resolveReplyFrom covers exact match, sub-address stripping,
catch-all detection, identity preference, and foreign-domain null.
2026-05-11 12:13:47 +02:00
Linus Rath f970fd1822 feat: add plugin hooks for compose, attachments, search, lifecycle, and routing 2026-05-02 21:27:56 +02:00
Linus Rath eecf16daa2 fix: stop silently destroying emails when trash mailbox isnt found #195 2026-05-01 20:01:13 +02:00
Linus Rath e9c9be84ad fix: preserve list scroll position when tagging an email 2026-05-01 17:18:11 +02:00
Linus Rath 123764f8b8 feat: improve new email notification logic for inbox 2026-05-01 01:55:24 +02:00
Linus Rath 7188abc9bc fix: set In-Reply-To and References on replies #234 2026-04-30 01:25:04 +02:00
Linus Rath 4788e8a91a feat: add right-click context menu to mail folders sidebar 2026-04-25 16:26:05 +02:00
Linus Rath 6c3529b368 fix: stop flicker on background folder refresh 2026-04-23 18:06:40 +02:00
Linus Rath 077a4f03a7 fix: preserve search/filter on batch move and archive 2026-04-23 18:01:21 +02:00
Linus Rath c30c38a7af chore: update version to 1.5.0 2026-04-23 00:07:34 +02:00
Linus Rath b7374570c8 feat: implement bulk email moving functionality in email store 2026-04-22 00:10:40 +02:00
Linus Rath 3c9fa5dc25 feat: implement batch archiving of emails 2026-04-22 00:05:38 +02:00
Linus Rath d4f7ae522e fix: use cid references for inline images #163 2026-04-18 00:57:46 +02:00
Linus Rath 8bdadc7ba3 fix: standardize punctuation 2026-04-16 19:07:42 +02:00
Linus Rath ad175d20e3 feat: enhance email deletion and spam handling with improved parameterization 2026-04-16 16:50:42 +02:00
Linus Rath f22699fe20 feat: add unified mailbox across accounts and sidebar icons toggle 2026-04-14 17:36:13 +02:00
Linus Rath 6678501501 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-04-13 00:51:21 +02:00
Linus Rath fa0045e01b fix: use onSuccessUpdateEmail to send before storing in Sent #188 2026-04-13 00:50:13 +02:00
Linus Rath ab57966a94 fix: search all folders when filtering emails by tag #175 2026-04-11 23:21:12 +02:00
Linus Rath f42f57b8d0 fix: preserve search filters on push-triggered mailbox refresh #164 2026-04-09 19:10:34 +02:00
Linus Rath 1eebec292a fix: enhance email loading and deduplication logic in email store #119 2026-03-30 08:32:46 +02:00
Linus Rath b5d471e9c8 feat: fix: enforce admin theme locks and repair theme ZIP bundles 2026-03-27 19:42:30 +01:00
Linus Rath 54981950b0 feat(plugins): resizable collapsible detail sidebar, auto-init on refresh
- Add email-detail-sidebar slot type for right-side plugin panels
- Plugin sidebar inside EmailViewer with ResizeHandle (200-500px, drag/dbl-click reset)
- Collapse/expand toggle with PanelRightClose/PanelRightOpen icons
- Call initializePlugins() on mount so plugins survive page refresh
- Fix plugin loader: call exposePluginExternals() before loading
- Fix CSP: add blob: to script-src for plugin bundle loading
- Fix enablePlugin: wire slot registration bridge before loading
- Fix email-banner PluginSlot: pass email via extraProps
- Fix sidebar-widget PluginSlot: move inside scrollable area
- Emit emailHooks.onEmailOpen/onEmailClose from selectEmail()
2026-03-25 00:44:05 +01:00
Linus Rath 0c1f182b6b fix: detect tasks created by external CalDAV clients (Thunderbird)
Tasks created in Thunderbird via CalDAV were not visible because
getCalendarTasks() used a strict @type === 'Task' check. Stalwart
may not set @type when converting VTODO from CalDAV to JMAP.

- Use case-insensitive @type matching for server variations
- Add fallback heuristic: detect tasks by presence of 'progress'
  property (exclusive to JSCalendar Task, never on Event objects)
- Normalize @type to 'Task' on detected tasks for consistent
  downstream handling
- Refresh task store on CalendarEvent state changes so tasks
  created externally appear without manual page refresh

Fixes #84
2026-03-24 14:54:37 +01:00