Commit Graph
294 Commits
Author SHA1 Message Date
Stefan HildebrandtandLinus Rath c1acf58c5f test(compose): add findComposeIdentityId to reply-identity mock
The recipient chip-drag and paste tests render <EmailComposer>, which since
b716f95a (feat(compose): preselect identity of the active mailbox) calls
findComposeIdentityId() from @/lib/reply-identity in compose mode. Both tests
mock that module but only returned resolveReplyFrom, so vitest threw
"No 'findComposeIdentityId' export is defined on the mock" on mount,
failing all 18 tests. Add the missing export (returns null; the composer
guards with if (composeIdentityId)).
2026-07-11 19:36:27 +02:00
honzupandLinus Rath 42798c2b7c fix: open signature links in a new tab instead of navigating the app away
Signatures render into the main document - the identity form's live preview
and the composer's signature block - rather than the sandboxed iframe used for
message bodies. SIGNATURE_SANITIZE_CONFIG allows no target attribute, so those
anchors were live and target-less: one click navigated the whole app away,
discarding the unsent draft or the unsaved signature with it.

Add sanitizeSignatureHtmlForDisplay, which keeps the storage sanitizer's image
restrictions but forces target="_blank" rel="noopener noreferrer" on every
anchor, and use it at the two render sites. The composer's SignatureBlock
NodeView stamps the target on its rendered DOM instead, because attrs.html is
what serializeEditorContent emits into the sent message - storage and the
recipient's copy stay exactly as the user wrote them.
2026-07-11 10:45:01 +02:00
dealerwebandLinus Rath c6bd5f645a Feature: contact groups as single expandable recipient chips
Typing a contact group's name in a recipient field suggested the
individual members, and "send email to group" on the contacts page
filled the field with one chip per member - the group itself never
appeared anywhere.

The autocomplete now offers the group as a single entry (group icon
plus member count), and selecting it - like the contacts-page action -
inserts one chip named after the group that carries a snapshot of its
members. The chip expands into the deduplicated member addresses when
the message is sent or saved as a draft, mirroring how Outlook handles
distribution lists. Expansion happens where the outgoing address lists
are built, so validation and every plugin hook see real addresses.

Group chips survive the composer's string boundaries (draft data, dirty
compare, the contacts-page hand-off) as RFC 5322 group syntax
("Team: a@x, b@y;"). A bare colon reliably opens a group there because
display names containing a colon are always quoted. Typed text only
parses as a group when it carries at least one valid member, so stray
"Subject: hello" input stays a plain recipient.

RecipientSuggestion gains an optional group field; plugins that ignore
it keep working unchanged.
2026-07-09 17:46:06 +02:00
Linus Rath d3f77ef9cf feat: add plugin ui.rerenderEmail API and restyle read-receipt banner 2026-07-09 16:04:33 +02:00
752e71198c feat: add 3 new plugin hooks : onBeforeBlobUpload, onBeforeDraftAutoSave, onBeforeEditDraft (#586)
Co-authored-by: Linus Rath <minipixxelinfo@gmail.com>
2026-07-09 14:55:34 +02:00
dealerwebandLinus Rath 94af4725b6 Fix: send mailto unsubscribe ourselves instead of via the OS handler
The List-Unsubscribe action for mailto: links created a hidden anchor,
clicked it and reported success. That hands the mailto: URL to the OS
default mail handler - for a webmail user that opens the wrong program
or nothing at all, and the unsubscribe message is never sent, while the
banner still claims it was.

The confirm flow now parses the mailto: URL (address, subject, body -
percent-decoded manually since RFC 6068 does not use plus-encoding) and
sends the message through the account's own JMAP client, preferring the
identity that received the newsletter so the list can match the
subscriber. In unified views the send is routed to the email's owning
account. Success is only reported once the server accepted the message.

The mobile confirm dialog reused the success strings as its question
text; it gets proper confirm_message strings in all 22 locales, and
success_mailto now says what actually happened.
2026-07-08 15:41:46 +02:00
Shuki VakninandLinus Rath 22418c17cf feat(i18n): Hebrew locale + full RTL support 2026-07-08 00:07:34 +02:00
Linus Rath c7d551f185 fix: attachment reminder ignores quoted text on reply/forward #570 2026-07-07 23:54:50 +02:00
Linus Rath 6470fa86f0 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-07-07 23:44:34 +02:00
Linus Rath 8647d709ff fix: only commit recipient on Space when input is a valid email #571 2026-07-07 23:43:54 +02:00
Shuki VakninandLinus Rath b73d1b55d1 fix(email): render emails that put height:100% on a wrapper element
Some HTML emails set height:100% on a full-bleed wrapper table/div rather
than html/body. With the viewer's body{overflow:hidden}, this collapses
documentElement.scrollHeight to the iframe's 150px default, so the
scrollHeight-based auto-resize locks the iframe short and the body renders
blank below the fold. A Box.co.il verification email rendered as a
logo-only 150px strip.

- Neutralise height:100% on any element so the body grows to its content.
- Measure max(documentElement, body).scrollHeight, and re-measure on a
  fixed cadence over a short settle window so a late reflow (height:100%
  wrapper, or images that resize after onload) is caught even when no
  ResizeObserver/image event fires.
2026-07-07 23:40:18 +02:00
honzupandLinus Rath 7db6fd7e24 feat: add setting to disable tag-color row tint in message list
Message-list rows are tinted with the first tag's color, which becomes
overwhelming when a label applies to most messages (for example
per-account labels). Add a `tintListRowsByTag` setting (default true, so
current behavior is unchanged) with a toggle in Settings beside
"Colorful Sidebar Icons". When off, rows are not tinted; tag dots and
chips still show the color. Gated in both list renderers
(email-list-item and thread-list-item).
2026-07-07 23:39:05 +02:00
Linus Rath c3a97de62f fix: use calendarAddress/organizerCalendarAddress for scheduling, drop retired sendTo/replyTo #500 2026-07-07 23:18:05 +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 d7a64fd9d6 Fix: hide the spam action in Sent, Drafts and Scheduled
Marking your own outgoing mail as spam makes no sense, but the action
was offered in every non-junk folder: context menu, hover quick-actions,
viewer toolbar and its overflow menu, plus the "!" shortcut.

All surfaces now skip the action when the folder role is sent, drafts or
scheduled, and the shortcut is a no-op there. Scheduled messages were
already covered per-email via isScheduled; the role check additionally
covers the server-side Scheduled folder before that annotation loads.

The hover quick-actions bar gets a spamApplicable prop for this, since
it renders its buttons without knowing the folder.
2026-07-06 11:04:37 +02:00
Shuki VakninandLinus Rath 6b74615969 fix(email-viewer): hide images that fail to load
Instead of leaving the browser's broken-image placeholder + alt text (which
reads as stray label text — e.g. a 'logo' alt — in an otherwise image-only
email), hide any image that fails to load. Sanitizer-blocked images already use
a 1x1 transparent pixel with display:none, so they're unaffected.
2026-07-05 21:29:19 +02:00
Shuki VakninandLinus Rath 902774eae1 feat(list): click sender avatar to select message/thread (Thunderbird-style)
Wrap the message-list avatar in a SelectableAvatar control: clicking the
avatar toggles the row into the current selection instead of opening it,
matching Thunderbird's correspondent-avatar selection affordance. A check
overlay appears on hover (hinting it is clickable) and stays while selected.

- email-list-item + thread single-email: toggle that message's id
- thread header: toggle the whole thread (reuses existing thread-select logic)
- focused-mail and extra-compact layouts render no avatar, so unaffected
2026-07-04 15:00:15 +02:00
dealerwebandLinus Rath e6aa79ed94 Feature: recipient autocomplete from Sent, with on-demand server search
Compose recipient fields only suggested existing contacts and directory
users, so people you had emailed before but never saved as a contact
never came up. This adds an Outlook-Web-style suggestion flow.

On startup the Sent folder is read once (metadata only) to build a cache
of addresses you have written to; those are merged into the autocomplete
after contacts and directory principals, deduped, contacts winning.

When the recipient is not in the cache, the dropdown offers a "search the
server" row that queries the Sent folder on demand. That lookup fetches
only the to/cc fields (no subject, body or attachments) and returns the
matching addresses, deduped.

New strings are added to all 20 locales.
2026-07-04 14:56:49 +02:00
KazNIISA ITandLinus Rath 396f5f7d60 fix(compose): wait for in-flight attachment uploads before sending
Clicking Send while attachments were still uploading silently dropped
them from the outgoing message: every place that builds the outgoing
attachment list filters on att.blobId && !att.uploading, and the Send
button never accounted for uploads still in flight. Attach a few files,
hit Send right away, and the email could go out missing some of them
with no warning.

handleSend now detects pending uploads before validating:

- Send is disabled with an explanatory tooltip
  (validation.attachments_uploading) while it waits.
- Once uploads finish cleanly, the send proceeds automatically - no
  second click needed.
- If an upload FAILS while waiting, the send is aborted with an error
  toast (validation.attachment_upload_failed) instead of silently
  shipping the email without the failed attachment - the user may not
  be looking at the composer to notice the red error chip.
- If the draft is closed or discarded while waiting, the pending send
  is cancelled cleanly.

The wait/decision logic lives in waitForPendingUploads() in
lib/email-composer-utils.ts (returns completed | cancelled | failed)
with unit tests covering all three outcomes. Outgoing-attachment
call sites read the freshest state via attachmentsRef since the
render closure captured at click time won't reflect uploads that
finished during the wait.

Both new i18n keys added to all 20 locales under
email_composer.validation.
2026-07-04 14:54:41 +02:00
Shuki VakninandLinus Rath f291480565 feat(email): send quick reply with Ctrl/Cmd+Enter
The message-viewer quick-reply box only sent via the Send button. Add a
keyboard shortcut (Ctrl+Enter / Cmd+Enter) mirroring the composer (#344),
so a reply can be fired without reaching for the mouse. preventDefault stops
the newline; the shortcut and button share one handleSendQuickReply().
2026-07-04 14:49:45 +02:00
Patrick RotterandLinus Rath b716f95a73 feat(compose): preselect identity of the active mailbox for new messages
Starting a new message while viewing a specific mailbox/account now defaults
the From identity to that mailbox instead of the global primary identity, so
composing from info@ sends as info@. Mirrors the existing reply-time identity
match and rides the same autoSelectReplyIdentity setting; reply/replyAll/forward
keep resolving from the original recipients. Matches exact then +tag-stripped.

Extracts findComposeIdentityId into lib/reply-identity.ts with unit tests.
2026-06-30 16:31:34 +02:00
Shuki VakninandLinus Rath 6c49427c7c fix(list): shift-click on the checkbox extends the selection (range)
selectRangeEmails was only wired to shift-clicking the row, but the
checkbox handler called stopPropagation and a plain toggle — so shift-
clicking checkboxes (the obvious affordance in selection mode) selected
single messages instead of the range. Make all three checkbox handlers
(email-list-item, thread single-email, thread header) shift-aware:
shift -> selectRangeEmails, otherwise toggle. Adds a regression test.
2026-06-30 09:35:20 +02:00
Kristofer PettijohnandLinus Rath 2a41e73bf9 fix(pro): prompt to save or discard draft when closing compose tab via tab-bar X 2026-06-30 06:51:24 +02:00
Linus Rath f8b8e0b108 refactor: move S/MIME to generic crypto plugin hooks 2026-06-28 19:13:02 +02:00
Linus Rath 5e67671f57 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-06-25 01:05:00 +02:00
Linus Rath 155d99a069 feat: add plugin hooks for email details, headers, and source 2026-06-25 01:04:26 +02:00
Stefan HildebrandtandLinus Rath d863b1fd4b fix: HTML-escape sender/subject in reply/forward quote header (#482)
The forward quote header renders "From: Name <email>", but the HTML variant
interpolated the sender string unescaped. In the rich-text composer the
"<email>" portion is parsed by the browser as a bogus HTML tag and dropped, so
the address silently disappears - the user sees only "From: Display Name". The
plain-text variant and the details panel escape correctly, which is why the
address shows there. This is the regression from #367, which added the
"<email>" into the HTML string without escaping it.

Fix: HTML-escape the user-controlled values (sender, subject, date) in every
HTML quote-header path - the production builder in lib/quote-header.ts and the
composer's inline fallback (both htmlBody and plain-body branches), for forward
and reply. The reply line keeps the bare display name by design (#367), but its
HTML form is now escaped too so a display name containing markup can't break
out. As a side benefit this closes an HTML-injection vector: a crafted subject
or display name was previously injected raw into the composer document.

Adds lib/__tests__/quote-header.test.ts covering: forward text keeps
"Name <email>"; forward HTML escapes the angle brackets (address survives) and
a markup subject/display name; reply stays bare-name and HTML-safe.
2026-06-25 00:25:28 +02:00
Linus Rath 1ff23790ae Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-06-24 19:56:15 +02:00
198407ebf5 fix(composer): keep HTML signature styling in the editor and on send
Rich, table-based identity signatures lost all their inline CSS
(background/text colors, fonts, border-radius, bgcolor). The composer
embeds the signature into the TipTap editor, and parsing it into the
ProseMirror schema flattened it to a generic bordered table. That
normalized version was then shown while composing AND delivered to the
recipient, even though Identity settings stored and previewed it
correctly.

Hold the signature as a dedicated, non-editable atom node
(SignatureBlock) that keeps the verbatim HTML in an attribute and renders
it inside a Shadow Root, mirroring the existing QuotedHtml island. The
markup is never parsed into the schema, so the styling survives 1:1 both
in the in-editor preview and in the outgoing mail
(serializeEditorContent inlines the verbatim HTML, as it already does for
quoted originals). The signature stays a single unit: select it and
Backspace/Delete to remove it; identity switching still swaps it via the
existing data-signature-block markers.

Adds unit coverage (parse + serialize round-trip preserves inline styles).

Fixes #475

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 19:56:10 +02:00
Linus Rath f972068143 fix: localize special-folder names by JMAP role #404 2026-06-24 19:32:13 +02:00
Linus Rath ae5d397512 feat: add "Download all" button to bundle attachments into a zip #466 2026-06-24 18:46:41 +02:00
Linus Rath 32f0a67dbc fix: gate Send-now toolbar label, translate send_now across locales 2026-06-24 16:52:36 +02:00
Shuki VakninandLinus Rath baf094d026 feat(scheduled): Send now button on scheduled/delayed messages
Adds a 'Send now' action to the scheduled-send view (both the toolbar and the
inline banner) so a queued message — whether explicitly scheduled or held by the
undo-send delay — can be sent immediately instead of only cancel/reschedule/edit.
Reuses the existing reschedule path (reschedules the submission to now), so no
new JMAP plumbing. Toolbar 'Cancel send' demoted to ghost so 'Send now' is the
single primary action. i18n added across locales.
2026-06-24 16:46:18 +02:00
Shuki VakninandLinus Rath 5f713a033b fix(mail-list): truncate long subjects so they don't overlap the timestamp
In the single-line (focused) message-list layout, the subject span used
`shrink-0`, which prevented `truncate` from engaging: a long subject sized to
its full content width and overflowed the bounded subject/preview group,
rendering on top of the timestamp on the right.

Let the subject shrink and truncate (`shrink-0` -> `min-w-0`), and give the
inline preview a high shrink factor (`shrink-[9999]`) so it collapses first —
the subject stays fully visible while there's room and only truncates with an
ellipsis once the preview is gone, never colliding with the time.

Applied to both email-list-item and thread-list-item (single-email and
thread-aggregate rows).
2026-06-24 16:33:43 +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
3dd596ba50 fix: guard compose Send against double-submit
Every Send control was disabled only by `canSend` (recipient/subject/body
validity), which never reflects an in-flight submission, so the composer
stayed interactive during the JMAP round-trip. Clicking Send quickly more
than once - or a click racing the keyboard send shortcut - invoked
handleSend once per click and sent the message multiple times (duplicate
deliveries and duplicate Sent entries), most easily hit on higher-latency
connections.

Add a synchronous re-entry guard: a ref (not state, which updates
asynchronously and wouldn't block a second click in the same tick) set once
handleSend clears its "don't send" early returns and reset in a finally,
plus an isSending state that disables every Send control. Covers all entry
points - the three Send buttons, the keyboard shortcut, the schedule dialog,
and the attachment-warning confirm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:42:39 +02:00
Shuki VakninandLinus Rath e50fe0d4db fix(mail-list): add breathing room between the unread dot and the avatar
The unread indicator dot was absolutely positioned at `left-1` (4px), leaving
only ~4px between it and the avatar (which starts at the row's `px-4` gutter),
so the dot read as flush against the avatar. Move it to `left-0.5` so it sits
nearer the panel edge (like other mail clients) and opens the dot-to-avatar gap
to ~6px. Applied to both email-list-item and thread-list-item (all three
absolute dot instances).
2026-06-22 17:10:46 +02:00
Linus Rath d0ed4b4dfe fix: block remaining email tracking vectors #457 2026-06-22 00:09:09 +02:00
Stefan HildebrandtandLinus Rath 3f9e60843d fix: repair pre-existing failing vitest suite
Fixes failures across the suite that fail on main independently of any branch.

Documented + skipped
- smime/smime-crypto: this suite OOMs its worker (~4 GB heap) generating and
  using real 2048-bit RSA keys via pkijs/asn1js — a pre-existing memory issue,
  not a logical failure. Skipped behind a single SKIP_SMIME_CRYPTO_OOM flag with
  an in-file explanation and re-enable instructions, and the beforeAll bails
  early so the skipped file runs in ~2s instead of crashing the worker.

Code fixes
- jmap/client: getSubmissionAccountId honoured the requested (mail) account
  even when it lacks the submission capability, so EmailSubmission/set was
  addressed to the wrong account when JMAP hosts submission in a separate
  account. Prefer an account that actually advertises submission, falling back
  to primaryAccounts['…:submission'].
- plugin-sandbox/loader: deactivateAllSandboxed used require('./registry'),
  which is unresolvable under the Vite/ESM test runtime. registry only imports
  types (no cycle), so use a static import; all() already returns a copy, so
  iterating while deregister mutates is safe.

Test fixes (tests trailed intentional code/behaviour changes)
- vitest.setup: add a matchMedia stub (jsdom lacks it) — unblocks 8
  email-list-item tests.
- calendar-utils: pin TZ=UTC for the timezone-sensitive bounds/layout assertions
  (host runs at UTC+2) and update expected minutes to UTC.
- calendar-participants: buildParticipantMap keys entries by generated UUIDs
  (RFC 8984), not 'organizer'/'attendee-N'. Look entries up by identity so the
  test no longer depends on a generateUUID mock leaking from another file.
- email-headers: softfail now returns the semantic 'text-warning' token.
- email-list-item: unknown keyword ids intentionally render a gray fallback badge.
- plugin-loader: exposePluginExternals is now a documented no-op.
- plugin-slot: PluginSlot reads the sandbox registry and renders iframe slots;
  rewrite the tests against that architecture with a referentially stable snapshot.
- plugin-types: MAX_THEME_SIZE was raised to 2 MB.
2026-06-19 23:53:20 +02:00
Stefan HildebrandtandLinus Rath 3516d3c727 chore: resolve react-hooks/exhaustive-deps warnings
Goes through the 7 exhaustive-deps warnings individually:

Added the genuinely-missing dependency (safe, no extra churn):
- email-viewer useMemo: add effectiveEmailContent.hasStyleTag (used for
  hasOwnLayout; changes in lockstep with .html, closing a latent staleness gap).
- pro-compose-tab-body handleSend: add refreshCurrentMailbox (stable zustand
  selector) and drop the stale fetchEmails/selectedMailbox deps — which left
  those two selectors entirely unused, so remove them too.
- use-mailbox-drop handleDrop: add sourceMailboxId (changes in lockstep with
  draggedEmails, already a dep).

Suppressed with a justified comment where depending on the whole object would
regress behavior — these are intentional fine-grained deps:
- email-composer signature-swap effect (keyed to signature fields + prev*Ref
  guards; whole signatureIdentity would re-splice the live editor).
- email-viewer auto-mark-as-read (whole email would reset the delay timer on
  any unrelated field update).
- email-viewer effective-attachments memo (derives from email.attachments;
  whole email would churn the list + its layout measurement).
- email-viewer auto-MDN effect (email already captured via id +
  sendReadReceiptNow; autoMdnRef guards double-send).

tsc --noEmit clean; eslint now reports 0 problems.
2026-06-19 12:31:07 +02:00
Stefan HildebrandtandLinus Rath 344795a8d9 feat: split a pasted address list into recipient chips
Pasting a list of addresses into To/Cc/Bcc now creates one chip per
address instead of dropping the whole blob in as a single invalid chip.
A paste is split only when it actually contains a separator; a lone
address falls through to normal editing.

- Separators: commas, semicolons, and any whitespace/newline - covers
  comma/space dumps, spreadsheet columns and Outlook-style `;` lists.
- Display names are preserved: `Name <email>`, a fully-quoted
  `"Name <email>"` entry, and `"Doe, John" <email>` (comma inside a
  quoted name) each stay a single chip with the name intact.
- Bare-address runs split per address; a `<addr>` token is unwrapped;
  tokens that aren't valid addresses are left behind in the input for
  the user to fix rather than becoming junk chips.
- Deduped case-insensitively within the paste and against existing chips.

Implemented as splitPastedRecipients in email-composer-utils, layered on
the shared quote/angle-aware splitter: splitRecipients gains an optional
`separators` argument so the composer/mailto serialization boundary
(comma-only) and the paste path (`,;\n\r`) share one implementation.
Wired into the recipient chip input's onPaste handler (To/Cc/Bcc).
2026-06-19 12:30:43 +02:00
Max HaoandLinus Rath c4f1cc23d7 fix blank space with plain-text emails 2026-06-16 15:48:50 +02:00
Max HaoandLinus Rath 4f1390fdeb fix toolbar re-render when opening emails 2026-06-16 15:48:50 +02:00
Max HaoandLinus Rath c0ca6d3102 fix: add collapse all threads functionality to email selection in thread list 2026-06-16 13:34:56 +02:00
Linus Rath 1f889b0965 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-06-15 23:16:10 +02:00
Linus Rath 0b9fe5451f fix: preserve line breaks in generated text/plain alternative #421 2026-06-15 23:15:04 +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
Stefan HildebrandtandLinus Rath 94b1f5aa48 refactor: model composer recipients as arrays instead of delimited strings
Alternative to the quote-aware string fix: represent committed To/Cc/Bcc
recipients as Recipient[] ({name?, email}) with a separate input-text
string per field, instead of a single comma-joined string parsed with
split(','). Structured recipients can never be torn apart on a delimiter,
so a display name containing a comma ("Doo, John <john@doo.org>", as
produced on Reply-All) stays a single chip.

- email-composer-utils: add Recipient type, parseRecipient/formatRecipient,
  and parseRecipientList/formatRecipientList for the (de)serialization
  boundary (ComposerDraftData stays a string; quoting keeps it lossless).
  Remove the now-unused string-chip helpers.
- email-composer: to/cc/bcc are Recipient[]; toInput/ccInput/bccInput hold
  the in-progress text. Reply/forward init, autocomplete, chip edit, drag &
  drop (payload now carries the structured recipient), send/draft/validation
  and template paths all operate on arrays. withInput() folds uncommitted
  typed text into the send/validation set.
- Tests updated for the array contract; add comma-in-name chip coverage.
2026-06-15 22:59:46 +02:00
Linus Rath dcea4fdd5e feat: show recipient address in chip drag preview 2026-06-15 15:28:03 +02:00
Stefan HildebrandtandLinus Rath 1ebfb286ad feat: drag and drop recipient chips between To/CC/BCC fields
Adds native HTML5 drag-and-drop so users can move recipient email
address chips between the To, CC, and BCC fields in the composer.
Chips dragged onto the Cc/Bcc toggle buttons auto-reveal the hidden
field and place the chip there.
2026-06-15 15:23:00 +02:00