350 Commits
Author SHA1 Message Date
Andrea d103c47cda fix(signatures): place caret above the signature in compose and above-quote replies
Publish Docker Image / prepare (push) Successful in 4s
Publish Docker Image / build (linux/amd64, ubuntu-latest) (push) Failing after 10s
Publish Docker Image / build (linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
Publish Docker Image / merge (push) Canceled after 0s
2026-08-24 10:36:24 +02:00
Andrea e696c65f75 fix(signatures): apply reply signature to replies and forwards
Publish Docker Image / prepare (push) Successful in 3s
Publish Docker Image / build (linux/amd64, ubuntu-latest) (push) Failing after 19s
Publish Docker Image / build (linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
Publish Docker Image / merge (push) Canceled after 0s
2026-08-23 19:04:49 +02:00
Andrea 33ca4bae37 fix(email): sender popover Email action opens in-app composer instead of mailto:
Publish Docker Image / prepare (push) Successful in 1s
Publish Docker Image / build (linux/amd64, ubuntu-latest) (push) Failing after 1m7s
Publish Docker Image / build (linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
Publish Docker Image / merge (push) Canceled after 0s
2026-08-23 17:24:29 +02:00
Bernd Rodler cfdd091d22 feat: Phase 3+4 — security hardening + polish + offline + Electron push
Phase 3 (security):
- P3.1: Feature gate server-side enforcement (403 on disabled features)
- P3.2: Unified auth error interceptor (401→logout)
- P3.3: Store-level state isolation via StoreSnapshot contract
  (added message-list-tabs + task stores to snapshot/restore cycle)
- P3.4: Push event bus extraction — email-store no longer imports
  calendar/contact/filter/file stores directly
- P1.3: Auth localStorage AES-GCM encryption via custom Zustand adapter

Phase 4 (polish):
- P4.1: Offline write queue — pending operations in localStorage,
  auto-retry on reconnect, offline-queue-indicator banner
- P4.2: Identity spoofing — fromOverrideEmail domain validation
- P4.3: WebSocket push for Electron via main-process IPC bridge
  (ws package with Authorization headers)
2026-08-07 22:10:26 +02:00
Bernd RodlerandClaude Sonnet 5 a0bffa9467 fix(compose): resolve TDZ crash in resolveStoreSignatureId
Introduced by the P2.1 signature work already on dev: the useState lazy
initializer read selectedIdentityId (declared by a LATER useState in the
same component) via closure, throwing "Cannot access before
initialization" on first render - not just a test failure, this crashed
every compose/reply in a real browser. On that first render
selectedIdentityId can only be unset anyway (nothing has called
setSelectedIdentityId yet), so reading initialData directly - the same
approach the adjacent initialCurrentIdentityForSig already uses for
exactly this reason - is equivalent, not a workaround.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 14:07:16 +02:00
Bernd Rodler 83e29b3ef1 feat: P2.2 Create Appointment from Email + P2.4 Calendar Dashlet + P2.12 Action Wheel + P2.14 Share Files
- P2.2: 'Create Appointment' button in email viewer → pre-fills event modal
  with subject, body, participants, date. calendar-store newEventPrefill state.
- P2.4: MiniCalendarDashlet in sidebar bottom — month grid with event dots,
  day click navigates to calendar. Collapsible, respect firstDayOfWeek.
- P2.12: Custom radial menu (components/ui/radial-menu.tsx) — circular SVG
  menu with keyboard nav, animations. Wired into email-list, contact-list,
  file-browser, calendar-month-view right-click handlers.
- P2.14: 'Send as Attachment' button in file browser — opens compose tab
  with selected files pre-attached via Pro tab store.
2026-08-07 13:15:04 +02:00
Bernd Rodler 67f61f18d0 feat: P2.1 extended signatures — multiple per identity + TipTap editor
- New stores/signature-store.ts: Zustand persist with CRUD, default/reply
  signature IDs, per-identity signature mapping
- New signature-settings.tsx: list management with add/edit/delete/duplicate
- New signature-editor-modal.tsx: TipTap rich text editor for signatures
- email-composer.tsx: auto-insert signature based on mode (compose/reply)
  + signature selector dropdown in toolbar
- identity-form.tsx: per-identity default/reply signature dropdowns
- settings/page.tsx: Signatures tab in Mail settings group
2026-08-07 12:55:14 +02:00
Bernd Rodler bde9f14832 feat(theme): ship exactly 2 themes (SRC default + VNClagoon); rebrand user-facing VNCmail+
Product decision 2026-08-05: exactly SRC (default) and VNClagoon ship as
selectable themes. Everything else (Qui, Nord, Catppuccin, Solarized,
Roundcube Elastic, Aurora Glass) is hidden via ThemePolicy.disabledBuiltinThemes
rather than deleted - cheap to re-enable later, zero code lost. Also removed
the hardcoded "Default/Bulwark" theme card from the Settings > Themes grid
(product wants exactly 2 theme choices, not 3).

Separately, found and fixed real "still says Bulwark" branding gaps:
- All 24 locale files: "Bulwark"/"Bulwark Mail"/"Bulwark Webmail" ->
  "VNCmail+" in every user-facing string (verified via the translations
  test, which only checks structural key parity across locales, not
  content - a straight string swap is safe against it. 48/48 still pass).
- PWA manifest fallback app name, package.json description/author.
- Demo mode fixtures: the "Welcome to Bulwark Mail!" email and identity
  signature a first-time demo user actually sees.
- The demo empty-state's logo was hardcoded to a literal Bulwark SVG file
  regardless of active theme - a real bug, not just stale text, since
  lib/theme-logo.ts's resolveThemeLogo() already exists and is already used
  correctly by the login page and nav rail for exactly this (theme-aware
  SRC mark / VNClagoon wordmark). Wired the same helper in here instead of
  a hardcoded path.

NOT touched: internal code comments referencing "Bulwark" as historical/
attribution context (e.g. design-rationale comments explaining why a value
differs from Bulwark's original) - those are harmless and a full-repo sweep
of every comment wasn't the ask.

Verified: typecheck clean, lint clean, translations test 48/48 passing.
2026-08-05 23:43:19 +02:00
Linus Rath 5a2bc6b671 Merge branch 'main' of https://github.com/bulwarkmail/webmail
# Conflicts:
#	app/api/dev-jmap/[...path]/route.ts
2026-07-30 19:17:43 +02:00
Linus Rath 59bc7fd64c fix: reply to own thread message addresses original recipients #703 2026-07-30 19:02:39 +02:00
Linus Rath 1cdbf75270 fix: use full tag path in drag-drop toasts, fresh email in context menu markAsRead
Nested tag toasts from drag-and-drop only showed the leaf name for
non-root tags, contradicting the comment above it and making two
same-named leaves under different parents (e.g. Personal/Receipts vs
Work/Receipts) indistinguishable in the toast.

The context menu's markAsRead handler was the one action left reading
the stale contextMenu.data instead of the live-refreshed
contextMenuEmail introduced alongside it, so it could act on outdated
email state while every sibling handler was already updated.
2026-07-30 18:39:49 +02:00
Mathy Vanvoorden f1e1ed1df7 fix: make the tint of selected rows work the same way in dark and light mode 2026-07-29 17:18:42 +02:00
Mathy Vanvoorden d9d9f91a86 feat: Make it easier to handle multiple tags
- Tags can now be removed straight from the email header
- Tagging control now allows the user to (de)select multiple tags in one go
2026-07-29 17:18:42 +02:00
Mathy Vanvoorden 108406a885 feat: improve visualization of tags
Previously tags where very much focused on color coding email and less about
adding additional information. They were also visualized in different ways in
different locations.

This commit gets rid of all "Color-coding" references, aligns visualization of
the tags across the whole project and tries to improve user experience of using
tags in general.

A search box is shown in the tagging control so the user can quickly search for
a tag if they have a huge (more than 10) amount of tags.
2026-07-29 17:18:42 +02:00
Mathy Vanvoorden 013ef7d557 fix: remove unused code
This code is nowhere used, so to prevent extra work during an upcoming refactor
of tags, it is removed and some related tests are now actually made useful
2026-07-29 17:18:42 +02:00
Mathy Vanvoorden ca0ba818b7 feat: Add nesting of tags in a tree
- levels are joined by forward slashes in the keywords
- behaviour is opt-in for now
- long paths are shortened if there is not enough display room

Closes #687.
2026-07-29 17:18:38 +02:00
Aaron Guise 0563e88eb8 fix: hide "Forward as attachment" in overflow menu when blobId missing
The overflow menu ("...") showed "Forward as attachment" whenever the
handler was provided, regardless of whether the open email has a
blobId. If it doesn't, handleForwardAsAttachment immediately no-ops
(buildForwardAsAttachmentPayload returns null), so the item was
clickable but did nothing - inconsistent with the list context menu's
version, which is already disabled in that case
(!onForwardAsAttachment || !email.blobId).

Gate both occurrences (desktop and mobile layouts) on email?.blobId
too, matching the context menu's behavior.

Caught by GitHub Copilot's automated PR review.
2026-07-27 16:52:54 +12:00
Aaron Guise 351f2d4e26 feat: add "Forward as attachment" to the message list context menu
Adds the same "Forward as attachment" action from the message viewer's
overflow menu to the right-click context menu on a message row in the
list, right after "Forward". Reuses the handleForwardAsAttachment
handler and buildForwardAsAttachmentPayload helper introduced earlier
in this PR - no new logic, just threading the prop down EmailList ->
EmailContextMenu the same way onForward already is.

blobId is already present on list-row emails (EMAIL_LIST_PROPERTIES
includes it specifically for the existing drag-out-to-filesystem .eml
export feature), so this works without any additional fetch. The menu
item is disabled if it's ever missing, matching how other actions
degrade when their handler prop isn't supplied.

Reuses the email_viewer.forward_as_attachment translation key already
added (via a second scoped useTranslations("email_viewer") call,
matching this file's existing cross-namespace pattern for
email_viewer.color_tag) rather than adding a duplicate key under
context_menu - avoids touching all 24 locale files again.
2026-07-27 16:37:04 +12:00
Aaron Guise 3ea22161d9 feat: add "Forward as attachment" next to Export as .eml
Adds a "Forward as attachment" action to the message overflow menu
(desktop and mobile), right beside the existing "Export as .eml"
action. Opens a new forward-mode compose window with the original
message attached as a message/rfc822 file instead of quoted inline -
useful for reporting spam/phishing to an upstream gateway that expects
the raw original as an attachment (the primary motivating use case:
gateways like MxGuarddog require complete original headers, including
the full mail path, for scanning), or for preserving a message's exact
formatting/headers when forwarding.

Implementation reuses the composer's existing attachment-carry-forward
mechanism (the `attachments` useState initializer in
email-composer.tsx already carries a forwarded message's own
attachments into the new compose via `replyTo.attachments`) - this
just adds one synthetic entry representing the whole original message,
referenced by its existing blobId. No re-fetch or re-upload needed,
since JMAP blobs are account-scoped rather than per-email. The inline
quote-header step (prepareComposerQuoteHeader) is skipped, so the body
starts blank instead of quoting the original.

The core "build subject + attachment entry" logic is extracted into a
pure, unit-tested helper (lib/forward-as-attachment.ts) rather than
left inline in the already-large page component.

Adds the forward_as_attachment locale key to all 24 locales (English
text as a placeholder pending translation, following the existing
add-a-key convention) to satisfy the translations completeness test.
2026-07-27 15:31:52 +12:00
dealerweb 21ea5009f4 i18n: localize the editor toolbar in all 23 locales
The rich-text editor was the last hardcoded-English surface in the
composer: 21 tooltip titles, the eight table-menu entries, the "Remove
color" entry and the table size picker's "Pick size" label were plain
strings while every other menu in the app is localized.

All of them now come from a new email_composer.toolbar namespace,
translated into all 23 locales using each platform's established
editor terminology (Word/Docs conventions - de "Formatierung löschen",
ar "مسح التنسيق", ja "書式をクリア", ...). The link prompt stays "URL",
which is the same term in every language.

Tooltips and self-sizing dropdowns have no width constraints, so longer
translations are safe everywhere.
2026-07-23 17:59:44 +02:00
dealerweb af2b00dd35 Fix: stop resurrecting deleted rows in the mailbox refresh merge
Fixes #592.

refreshCurrentMailbox merges the refreshed first page with the already
loaded list, appending existing entries beyond a cutoff. That cutoff
was derived from the refreshed list's length - so whenever a folder
shrank, the fresh page was shorter than the stale list and the loop
re-appended the deleted rows from stale local state, despite the
comment right above promising the opposite.

The visible result is the reported bug: after sending a draft, the
Drafts view keeps showing a ghost row for the already-destroyed draft.
The send actually succeeded - resending the ghost delivers the mail
again, which we reproduced with a live JMAP trace: four successful
submissions, an empty server-side Drafts folder, a notFound ghost id,
and five delivered copies. Deriving the cutoff from the page size
fixes the shrink case while preserving the merge's intent for
arrivals and loaded deeper pages; regression tests cover all three
shapes.

Also surface post-send filing failures instead of dropping them, as
flagged in 4dc76bbb's follow-up note: a rejected onSuccessUpdateEmail
patch or old-draft destroy now logs the server's error details and
returns a filingError on SendEmailResult, and the UI shows a warning
toast (all 23 locales) so a stale draft row is never again mistaken
for a failed send. A plugin veto of the send leaves a debug trace.
2026-07-23 13:26:05 +02:00
Linus Rath b7c8cd999e feat: collapse quoted reply text behind a "..." toggle #480 2026-07-22 19:17:23 +02:00
Linus Rath 813185e58d test: fix broken suites 2026-07-22 18:56:09 +02:00
Linus Rath 5a8c69dac2 fix: insert mail template at caret in replies instead of prepending #539 2026-07-22 17:53:40 +02:00
Linus Rath 5105e000f5 feat: add message-list category tabs 2026-07-22 17:22:20 +02:00
Linus Rath a909593dda fix: detect typing inside the QuotedHtml shadow island via composedPath #654 2026-07-21 23:26:51 +02:00
xhzeemandLinus Rath 23a017d4b7 fix(rtl): set dir=ltr on identity <option> elements
The From-identity picker in the composer and the template form use a
native <select>, so the earlier <bdi> fix can't apply there - browsers
render <option> as plain text and strip any nested markup. The native
OS-rendered option list still respects the dir attribute directly
though, so setting dir="ltr" on each option fixes the same bracket-
mirroring bug for "Name <email>" entries in that native popup.
2026-07-21 23:08:02 +02:00
xhzeemandLinus Rath c7250dc921 fix(rtl): isolate Latin address text ("Name <email>") from RTL bidi reordering
Unicode's bidi algorithm treats < and > as mirrored characters. When a
plain "Name <email>" string is rendered as a text node inside an
RTL-inherited container, the browser swaps and reorders those brackets
for the whole run, producing garbled output (e.g. "<Maria Lopez
<maria.lopez@company.example" instead of "Maria Lopez
<maria.lopez@company.example>").

Wrapped the affected text in native <bdi>, which auto-detects its own
paragraph direction from its content rather than inheriting the
ancestor's - so a Latin address renders LTR and a genuinely
Arabic/Hebrew/Farsi name still renders RTL, both correctly, in:
- recipient-popover.tsx (shared by the email viewer's From/To/Cc/Bcc
  detail rows and the calendar invitation banner's organizer row)
- email-composer.tsx's read-only From display
- eml-preview.tsx's From/To header lines

Left the equivalent <select><option> cases (composer identity picker,
template identity picker) and the composer's quote-header text (which
becomes actual email body content, already isolated per-paragraph by
the existing TextDirection tiptap extension) out of scope - both need
a different fix approach than <bdi>.
2026-07-21 23:08:02 +02:00
xhzeemandLinus Rath 80f76abc38 fix(rtl): flip JS-positioned popovers (storage, logout, account switcher, calendar picker)
These popovers are portaled and positioned via inline styles computed
from getBoundingClientRect() rather than Tailwind classes, so the
logical start-0/end-0 fix doesn't reach them. They always anchored to
the physical right of their trigger (rect.right + 8), which in RTL
pushes them further into the edge the trigger is already flush against
instead of toward the visible content area.

Added isDocumentRTL() to i18n/direction.ts and used it to mirror the
computed position in:
- navigation-rail.tsx: storage quota popover, logout/switch-account menu
- account-switcher.tsx: both the rail and expanded-sidebar variants
- calendar-invitation-banner.tsx: the "add to calendar" picker
2026-07-21 23:08:02 +02:00
xhzeemandLinus Rath adb8686293 fix(rtl): anchor floating menus with logical start/end instead of left/right
Popovers and dropdown menus across the app (sub-address helper, calendar
toolbar/color pickers, contact/template/attachment menus, rich text editor
color and table pickers, unsubscribe confirmation, composer send menu)
were anchored with physical `left-0`/`right-0`. In RTL locales those
don't flip with the trigger, so the menu detaches from the button that
opened it. Switched to Tailwind's logical `start-0`/`end-0` (and the
matching `rounded-s-*`/`rounded-e-*` corners on hover-action overlays)
so they mirror correctly for RTL locales (ar, he, fa) while staying
identical in LTR.
2026-07-21 23:08:02 +02:00
Shuki VakninandLinus Rath 53461d1142 feat(email-viewer): message spacing setting (auto/always/edge-to-edge) 2026-07-21 23:06:43 +02:00
Stefan HildebrandtandLinus Rath d5017a211f feat(email): open external links in a new tab (safely)
External web links (http/https) in rendered email bodies open in a new browser
tab with target="_blank" rel="noopener noreferrer". mailto:, tel:, and in-page
#anchors keep their default behavior instead of spawning a blank tab.

The plaintext render path is already handled on main by #594 (ADD_URI_SAFE_ATTR
in PLAIN_TEXT_RENDERED_CONFIG), so this no longer adds its own hook there — the
plaintext linkifier only ever emits http(s) anchors, so the config's declarative
exemption is sufficient. This change covers the paths #594 did not:

- iframe HTML render: both anchor passes (the DOMPurify hook and the post-render
  DOM walk in email-viewer.tsx) set target=_blank on EVERY <a>, including
  mailto:/tel:. Now scoped to http(s) via the shared applyNewTabToAnchor()
  helper (http/https -> target+rel; mailto/tel/#/other -> strip target/rel).
- sanitizeI18nHtml: the same DOMPurify strip dropped target/rel from translated
  links (e.g. the docs link in settings.security.not_available, target="_blank"
  in 19/22 locales). Keep the author's target and harden rel="noopener
  noreferrer".

Tests: unit coverage for isHttpLinkHref / applyNewTabToAnchor / sanitizeI18nHtml
plus an integration suite over the real plaintext and HTML/iframe render
pipelines. The plaintext-hook-specific cases are dropped as redundant with #594.
2026-07-21 20:59:20 +02:00
dealerwebandLinus Rath a779e101e6 Fix: label the close-dialog draft button with the generic Save
"Save Draft" made the third button of the save-or-discard dialog wrap
onto two lines in several languages (German "Entwurf speichern", French
"Enregistrer le brouillon", ...) while its siblings stay one line. The
dialog title already says the draft is what's being saved, so the
button now uses the existing generic common.save key - one short word
in every locale, no new translations needed.

email_composer.save_draft had exactly this one consumer; the dead key
is removed from all 22 locales.
2026-07-21 20:57:29 +02:00
Shuki VakninandLinus Rath c38bcc4a95 feat(email-list): add bulk Not-Spam action to selection toolbar in junk 2026-07-21 20:57:08 +02:00
Paulhenry SauxandLinus Rath 18e9cf6ee6 fix: use fixed tailwind classes for chips icon 2026-07-20 20:23:11 +02:00
Paulhenry SauxandLinus Rath 2cb5c739b4 feat(plugins) : add onRecipientChipsChange hook 2026-07-20 20:23:11 +02:00
Marc SportielloandLinus Rath 0a30b2fb3a feat(templates): add support for HTML templates 2026-07-19 10:10:31 +02:00
Marc SportielloandLinus Rath 0b62afb0f8 fix(email-composer): hide template buttons when templates are disabled 2026-07-19 10:08:09 +02:00
Linus Rath 4a4950c3e5 Fix: keep signature when inserting a template #621 2026-07-16 20:13:07 +02:00
Linus Rath 739b72d251 Merge pull request #509 from hildebrandttk/feat/unified-mailbox-account-scope
Feat/unified mailbox account scope

Rework the sidebar "All accounts" into an account-bounded "Unified Mailbox"
by default, with cross-account merging as an opt-in (admin-gated) sub-option.
The standalone per-account "All Mail" virtual folder is folded into the unified
All mail / Unread / Starred entries.

Conflict resolution notes:
- stores/settings-store.ts: both main and this branch independently added a
  per-account default-identity (#507) migration at different versions (main v6,
  branch v7). Merged migration is version 7 using the refactored migrateSettings
  function; the unified-mailbox rework is guarded at `version < 7` so users who
  stopped at main's interim v6 identity bump still receive it, while the #507
  identity-map coercion stays at `version < 6` so their populated map is kept.
- stores/auth-store.ts: kept main's applyPreferredIdentity (superset with the
  pre-#507 legacy migration).
- stores/email-store.ts: removed the ALL_MAIL_MAILBOX_ID paths (folded into the
  unified views) while preserving main's plugin hooks (onSearchResults /
  onEmailsFetched); adopted advancedSearchCrossViewEmails for advanced cross-view
  search.
- components/settings/layout-settings.tsx: kept main's faviconUnreadBadge setting
  alongside the new unifiedCrossAccount toggle.
- integration/: union-merged the two independently-authored suites - branch suite
  is authoritative (matches new behavior) with main's shared-identity (#569) group
  infrastructure preserved.
- components/email/email-composer.tsx: dropped a duplicate data-testid attribute
  introduced by the auto-merge.
2026-07-16 19:57:51 +02:00
Linus Rath 682e47c970 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-07-16 18:08:40 +02:00
Linus Rath a6d8671306 Fix: render email body on DOM parse, not iframe load #635 2026-07-16 18:08:14 +02:00
dealerwebandLinus Rath 6f278845f3 Feature: text color picker in the composer toolbar
The rich-text editor already registers the TextStyle and Color
extensions so that colored text pasted or quoted from incoming mail
survives editing - but there was no way to set a color yourself.

Adds a "Text color" toolbar button next to the strikethrough control,
wired to the already-loaded extensions: a 2x8 preset swatch grid plus a
"Remove color" entry, following the table button's dropdown pattern
(wrapper ref, outside-click close, same popover styling) and the table
size picker's swatch grid. The button's baseline icon renders in the
currently active color, so the selection is visible without any extra
indicator element.

No new dependencies and no locale changes; the toolbar titles in this
file are plain English throughout, and Clear Formatting already removes
colors via unsetAllMarks.
2026-07-16 18:01:18 +02:00
Stefan HildebrandtandLinus Rath 578339c400 test(integration): composer From offers shared/group identities (#569)
Provision a Stalwart group (team@example.org) with carol as a member before her
first login, and assert the composer's From selector offers the group address.
This confirms the group-membership scenario of #569 already works out of the
box: Stalwart returns the group's send-as identity on the member's own account,
so the app's normal single-account identity load surfaces it (identities.length
> 1 -> the From <select> renders with team@).

- stalwart: create the `team` Group in plan-accounts and add carol via
  User.memberGroupIds in the entrypoint (id resolved after apply, like
  DOMAIN_ID). carol, not alice/bob, so the sync specs stay unshared.
- helpers: GROUP config, openComposer/composerFromOptions, and JmapClient
  accounts + sharedAccountNames (Identity/get needs the submission capability).
- composer: add data-testid="composer-from" to the From <select> and its
  single-identity <span> fallback.

Ref: https://github.com/bulwarkmail/webmail/issues/569
2026-07-16 17:59:52 +02:00
Stefan HildebrandtandLinus Rath 8d8bc7cb13 test(integration): dockerized webmail⇆Stalwart Playwright sync suite
Add an end-to-end integration harness that runs the webmail against a real
Stalwart mail server in Docker and drives it with Playwright, focused on the
mail/folder synchronisation behaviour (unread/total counters, folder-list
sync, account-scoped Unified Mailbox) that the unified-mailbox work touches.

- integration/ stack: Stalwart (declarative bootstrap: alice/bob/carol,
  submission + IMAP listeners, permissive CORS) + webmail (dev mode, so the
  browser's plaintext cross-origin JMAP calls aren't blocked by the prod CSP).
- Helpers: dependency-free SMTP submit client, JMAP client for seeding /
  inspecting server state, and page helpers (login, add/switch account,
  locale-independent folder-counter reads).
- Specs: login, single-account sync (receive/read/move/delete/folder-create/
  burst) and multi-account (per-account isolation + cross-account Unified
  Inbox aggregation + background-account delivery). 12 tests, all green.
- Add focused data-testid hooks to the mail UI (folder rows + counters, email
  list items, account switcher, composer) for stable selectors.
- Exclude examples/ and integration/ from tsconfig/eslint/.dockerignore.
2026-07-16 17:59:52 +02:00
Shuki VakninandLinus Rath 04444003b2 feat(composer): auto-detect paragraph text direction by default 2026-07-16 17:56:50 +02:00
Joe PolastreandLinus Rath 7da3d4ae80 fix(email): show quote bar in email replies
Without the inline style in the serialized wrapper, the email reply quote bar gets lost (becomes invisible). Pull the style out into a const, and then use it in both the editor (NodeView) and the content wrapper for the email content that is sent.
2026-07-16 17:56:38 +02:00
Jesper OrdrupandLinus Rath 511f9e5195 fix: enable thread expansion in focused list 2026-07-14 16:20:52 +02:00
Paulhenry SauxandLinus Rath 432ba0516b fix: add bodyValues to onRenderEmailBody hook 2026-07-13 21:21:07 +02:00
Stefan HildebrandtandLinus Rath 37152504b4 feat(composer): drag-to-reorder To/Cc/Bcc recipient chips (#593)
Recipient chips could already be dragged between the To/Cc/Bcc fields, but a
drop always appended and same-field drops were a no-op, so recipients could not
be rearranged without deleting and re-adding them.

Add positional drag-and-drop: while dragging a chip, an insertion caret shows
the gap it would land in (based on which half of the hovered chip the pointer
is over, mirrored for RTL); dropping inserts it there.

- same-field drop reorders the chip locally (via onChipsChange), using the
  source index carried in the drag payload (fromIndex) and adjusting for the
  removal shift; dropping onto its own position is a no-op;
- cross-field drop inserts at the drop position: handleMoveChip gained an
  optional toIndex (omitted = append, e.g. dropping onto a hidden Cc/Bcc
  button, preserving existing behaviour);
- per-chip onDragOver computes the target gap; the container handles the
  trailing gap (past the last chip / over the input).

No new user-facing strings (the caret is purely visual), so no locale changes.

Tests (components/email/__tests__/recipient-chip-drag.test.tsx): reorder to end
/ front, self-drop no-op, cross-field positional insert, and caret visibility.
Also add the missing findComposeIdentityId export to the reply-identity mock in
the recipient drag/paste suites so <EmailComposer> mounts in compose mode.
2026-07-13 21:19:37 +02:00