Plain-text bodies render into the main document rather than the sandboxed
iframe, so an anchor without target="_blank" navigates the whole app away
instead of opening a new tab.
plainTextToSafeHtml emits target and rel correctly, but
sanitizePlainTextRenderedHtml stripped both back off: DOMPurify URI-tests
every attribute value not on its URI-safe list, and "_blank" does not match
PLAIN_TEXT_RENDERED_CONFIG's ALLOWED_URI_REGEXP. EMAIL_SANITIZE_CONFIG avoids
this only because its regex carries a catch-all alternation for non-URI values.
Mark target and rel as URI-safe so they survive the URI test, rather than
loosening href validation.
Fixes#588.
Sign-out already cleared the token-refresh timers and stopped the
keep-alive interval - the reported endless loops came from async
callbacks that were in flight at that moment. The token refresh's
failure handler re-armed its retry after logout, and a failing
keep-alive ping called reconnect() -> connect(), which restarts the
keep-alive and thereby revived the interval disconnect() had just
stopped. Only closing the tab ended it.
Two mechanisms fix that class: transiently failed token refreshes only
re-arm while the account is still signed in (checked when the failure
lands, not when the request started), and the client carries an
intentionallyDisconnected flag set by disconnect() - the ping callback,
reconnect(), the SSE reconnect scheduling and the polling fallback all
stop at it, so nothing revives after an intentional sign-out.
Failed retries also back off instead of hammering a down server every
30 seconds: the token refresh climbs 30s/1m/2m/5m (capped, reset on
success), and the keep-alive skips upcoming ticks on consecutive
failures for the same effective ladder. Recovery after an outage is
unchanged in substance - the session survives and reconnects within at
most ~5 minutes, immediately on user activity.
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.
The translation parity test fails on main again after the Jalali
calendar landed: the two newest locales, he and sk, were missed when
the twelve Jalali month names were added - they get the same Latin
transliterations every other non-Persian locale received.
fa in turn carried four keys that do not exist in en and are not
referenced anywhere in the code (the email_composer.text_direction
block and settings.templates.image_too_large) - removed, as the
suite's no-extra-keys check demands.
* feat: add Jalali (Persian/Shamsi) calendar support with Saturday as week start
- Add jalaali-js library for Gregorian ↔ Jalali date conversion
- Create lib/jalali-utils.ts with Jalali calendar utilities
- Create hooks/use-calendar-locale.ts for unified calendar locale handling
- Expand FirstDayOfWeek type to include 6 (Saturday)
- Update all calendar views (month, week, day, mini, toolbar) to support
Jalali calendar display and Saturday-first week ordering
- Add Jalali month names (Farvardin … Esfand) to all locale files
- Add Persian (fa) locale with full translations
- Update settings UI to include Saturday as first day of week option
- Update useFormatEventDate to show Jalali dates when locale is fa
- Auto-detect Jalali calendar when fa locale is active
The calendar system automatically switches to Jalali when the locale is
set to Persian (fa). All internal date handling remains Gregorian (ISO
8601) for JMAP protocol compatibility; Jalali conversion is purely at
the display layer.
* Add PR template for Jalali calendar feature
* chore: remove accidentally added PR template
* fix: add image_too_large key to fa locale for PR #462 compatibility
The translation parity test fails on main since the Hebrew locale
landed. Three gaps, all from catalogs drifting past each other:
The new he locale was based on an older en catalog and was missing 17
keys (pin/unpin, recipient autocomplete, attachment-upload validation,
the date_locale block, the tint_list_rows and show_folder_total_count
settings, language.sk). They are translated to Hebrew here; date_locale
keeps the same English values every other locale currently has.
The rtl_editing setting existed only in en and he - added translated to
the other 20 locales.
language.he was missing everywhere - added as the endonym "עברית" to
all locales, matching how the other language names are written.
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.
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.
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).
Any transient failure used to end the session: the token route deleted
the refresh cookies on every non-OK answer from the OAuth endpoint,
refreshAccessToken logged out on any non-OK status or network error,
and the startup restore evicted the account and deleted its session
cookie. A server restart, a proxy hiccup, a Wi-Fi switch or a laptop
waking before the network is back all kicked the user out despite
"stay signed in".
Failures are now classified. Only a definitive rejection (400/401/403
from the OAuth endpoint, 401 from the token route) tears the session
down and deletes cookies, exactly as before. Network errors and 5xx
keep the session: the token refresh re-arms itself and retries every
~30 seconds until the server is back, and the startup restore keeps
the account, marked unreachable - the same treatment the rate-limit
carve-out (#104) already applies.
Token validity stays entirely server-enforced: the first definitive
401 after an outage still logs out as before.
The Slovak translation was based on a slightly older en catalog, so the
translation parity test currently fails on main.
sk was missing ten keys that landed around the same time: the composer
attachment-upload validation strings, the recipient autocomplete
strings (translated to Slovak here) and the settings date_locale block
(kept as the same English values every other locale currently has).
The other twenty locales were missing language.sk in return - added as
the endonym "Slovenčina" (matching how the other entries are written),
and as "Slovacă" in ro, which translates its language names.
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.
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.
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.
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.
The page CSP set font-src 'self', which blocked fonts referenced by
rendered email CSS (brand webfonts loaded over https). Allow https:/data:
for font-src. Email bodies render inside the sandboxed iframe, which keeps
its own stricter blocking-mode font-src for privacy.
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
Login header customization for white-label deployments, all defaults
preserve current behaviour:
- LOGIN_LOGO_MAX_HEIGHT / LOGIN_LOGO_MAX_WIDTH (any CSS length): the logo
box is otherwise a fixed 64x64 (w-16/h-16), which fits a wide wordmark to
~13px tall. When either is set, the fixed box is dropped and the logo
renders at the configured size.
- LOGIN_SHOW_HEADING / LOGIN_SHOW_SUBTITLE (default true): hide the
{appName} heading and/or the subtitle when the logo already reads as the
brand (e.g. a wordmark) and they'd be redundant.
Applied to the standard login header; wired through the existing config
registry (CONFIG_ENV_MAP) -> /api/config -> useConfig.
Refs #519.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
The service worker hard-coded the notification icon and badge to the bundled /icon-192x192.png, so push notifications always showed the default Bulwark logo even when an admin had configured a custom PWA/favicon icon (which the manifest already honors via /api/pwa-icon).
Point both notifications at /api/pwa-icon/192, and make that endpoint fall back to the bundled default icon instead of returning 404 when no custom icon is set - so it always returns an app icon and the service worker (which can't run the custom-vs-default check itself) has a single stable URL.
Extend the layout-agnostic handling to the symbol shortcuts. On non-Latin
layouts the characters '/', '?', '#', '!' are often unreachable or on different
keys, so map them from their US-QWERTY physical codes (Slash, shifted Digit1 /
Digit3). Fixes e.g. '?' (open shortcuts help) on a Cyrillic layout.
Shortcuts were matched against event.key, which returns a layout-dependent
character. On non-Latin layouts (Cyrillic, Greek, ...) the physical letter keys
produce non-Latin characters, so single-letter shortcuts (c, j, k, r, e, ...)
never fire and users must switch layout to use them.
Derive the letter from event.code (KeyA..KeyZ) instead, which is
layout-independent. Non-letter keys keep event.key (arrows/Enter are already
layout-independent; #, !, ?, / stay symbol-based).
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.
This reading setting shipped with its English label/description as a placeholder in every locale except fa. Translate it into the remaining 18 locales (de, cs, da, es, fr, hu, it, ja, ko, lv, nl, pl, pt, ro, ru, tr, uk, zh), reusing each locale's existing 'mark as unread' wording for consistency.