Commit Graph
28 Commits
Author SHA1 Message Date
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 3f22a3323a i18n: add missing translation keys across 22 locales 2026-07-22 18:45:22 +02:00
Linus Rath b354319b82 feat: support HTML body in vacation responder 2026-07-22 17:56:05 +02:00
Shuki VakninandLinus Rath 6dfcb07b9a feat(accounts): remove a specific account from the switcher
The switcher only offered 'sign out of active' and 'sign out of all' — no
way to drop a single non-active account (e.g. one stuck in an error state you
can't switch into to sign out). Add a hover × on non-active, non-default rows
and a removeAccount(id) auth action that tears down the client, drops it from
the registry, and clears its per-slot session/token cookies.

Stacks on the switcher redesign in #517.
2026-07-21 20:59:52 +02:00
Shuki VakninandLinus Rath f51ec50443 feat(settings): add "Refresh cached data" recovery action
When the mailbox view gets into a stale or wrong state, the only escape
was the browser's "clear site data" — which also wipes the saved account
list, forcing a re-login of every account.

Add a non-destructive "Refresh cached data" button under Settings →
Data. It clears the server-derived caches (contacts, calendars,
identities, per-account snapshots) and reloads so they re-fetch fresh,
while preserving accounts, sessions, settings, themes and user content
(templates, S/MIME). Two-click confirm to avoid an accidental reload.

English strings added across all locales (translation follow-up); unit
tests cover the cache-clear (keeps account-registry/auth/prefs) and the
reload.
2026-07-21 20:58:55 +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
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
honzupandLinus Rath 334fdbfb86 feat: show unread count badge on favicon
Closes #560.

Composes the active inbox's unread count over the base favicon as an SVG
badge, served as a percent-encoded data: URL, so new mail is visible on a
tab that is not focused — including when the browser collapses tabs to
icon-only, where a title-based count disappears entirely.

The base icon is read from the rendered <link rel="icon"> rather than from
config, so admin and per-domain branding overrides are inherited for free:
the count is drawn on whatever logo the deployment actually serves. Keeping
the badge in SVG rather than rasterising to a canvas also means the browser
can rasterise it at whatever size it asks for, so a HiDPI tab is not served a
16px bitmap.

Notes on the approach:

- The badge link is an *additional* icon link that we append and mark as
  ours; we never remove or mutate a link we did not create. Next's metadata
  icons are rendered by React, which keeps a fiber pointing at that DOM node,
  so removing it would leave React holding a detached node and throw
  "Cannot read properties of null (reading 'removeChild')" on the next
  commit that deletes the fiber. Appending instead means the last-declared
  icon wins, and non-SVG fallback links survive with their type/sizes intact.
  (The usual recipe for this feature — assign canvas.toDataURL() to the
  existing link's href — does both of the things that break here.)

- Every change of state is an *insertion* of a fresh link of ours, never a
  mutation or a removal, because that is the only signal a browser reliably
  re-reads the favicon on. Firefox ignores an in-place href change, and it
  equally ignores a removal — so clearing the badge by deleting our link left
  a stale count painted on the tab until a hard reload. Clearing it instead
  inserts a new link of ours carrying the original base href.

- Holding last place has to be defended: on a client-side navigation React
  re-hoists its metadata icon link into <head>, landing after ours, and the
  base icon silently wins again. A MutationObserver on <head> moves our own
  link back to the end whenever a foreign icon link appears — moving only our
  node, never anyone else's. It no-ops once ours is last again, so a move
  cannot feed itself.

- The badge is a full-width band across the foot of the icon, drawn to the
  metrics measured from Gmail's own 16px favicon: band height 0.625 of the
  icon, digit cap height 0.44, flush to the edges, corners rounded by about a
  pixel. Full width is what keeps a three-glyph label legible — rounded ends
  waste exactly the horizontal space it needs. Neutral white with black digits
  rather than the conventional red: faviconUrl is admin-overridable and
  Bulwark's own icon is rgb(219,45,84), so a red badge sat red-on-red.

- The base SVG may be admin-uploaded, and the branding route deliberately
  serves it under a sandboxing CSP because SVG can carry script. Re-emitting
  it as a same-origin data: URL would un-fence that, so script, foreignObject
  and every on* handler are stripped before serialising.

- Mounted in the root layout, not on the mail route: the badge belongs to the
  tab, so mounting it on the page would clear it on every hop to settings,
  calendar or contacts.
2026-07-16 18:00:41 +02:00
Stefan Hildebrandt 7c221c4a4a feat(unified-mailbox): account-bounded Unified Mailbox with opt-in cross-account
Rework the sidebar "All accounts" section into a "Unified Mailbox" that, by
default, stays within the active login account and its shared/group folders.
Merging across multiple logged-in accounts becomes an opt-in sub-option instead
of the default, and the standalone per-account "All Mail" virtual folder is
folded into the unified All mail / Unread / Starred entries (its folder selection
now narrows those lists).

Scope:
- lib/unified-mailbox.ts: UnifiedAccountClient.crossIncludedMailboxIds; the cross
  views honor the per-account folder selection (union across accounts = the sum
  of each account's selection), falling back to inbox+custom when unset.
- stores/email-store.ts: buildUnifiedAccountClients gains scopeToClientAccountId
  (the account boundary) and populates crossIncludedMailboxIds from
  allMailFolderIds; remove the standalone __all_mail__ fetch/search/load-more
  branches.
- page.tsx: scope to the active account unless cross-account is active (per-user
  opt-in AND admin gate); the per-role unified mailboxes obey the same scope.

Folding:
- Drop ALL_MAIL_MAILBOX_ID (lib/jmap/types.ts); thread-list source-folder column
  now keys on isUnifiedView only; settings folder picker moves under the unified
  group and shows once any unified entry is enabled.

Config:
- User: new unifiedCrossAccount (default false); includeGroupInUnified default
  flips to true; enableAllMailView retired; the three cross-view toggles now gate
  the unified Unread/Starred/All mail entries.
- Admin: new unifiedCrossAccountEnabled gate, default FALSE (cross-account is an
  admin opt-in; when off the per-user toggle is hidden and the scope is forced
  account-bounded at runtime). allMailViewEnabled deprecated and normalized
  forward into crossAllViewEnabled on policy load; cross-view gate labels reworded
  to "Unified Mailbox: ...".

Header: the sidebar section shows "All accounts" when cross-account is active
(opt-in AND admin gate AND >1 connected account), else "Unified Mailbox".

Migration:
- Settings persist v5 -> v6 (exported migrateSettings) - cross-active users keep
  cross-account; All-Mail-only users get the account-bounded unified All mail
  entry with folder ids preserved; includeGroupInUnified enabled for every
  migrated config; fresh installs are account-bounded.
- Admin policy: one-shot, marker-guarded migratePolicyUnifiedMailbox (run before
  configManager.load) enables unifiedCrossAccountEnabled when a cross view was
  active, so existing cross-account installs keep the behaviour despite the
  default-false gate. Skipped on read-only config dirs.

Locales: sidebar all_accounts (original label) + unified_mailbox (translated, per
locale) keys; dead standalone all_mail strings removed across all 20 locales.

Docs: FEATURES.md updated to the account-bounded model, the cross-account gate,
and the folder-narrowed aggregate entries.

Verification: tsc clean, eslint clean, full vitest suite green (incl. translations
completeness, cross-view/migration coverage, and the admin policy migration test).
2026-07-11 21:14:34 +02:00
Linus Rath 2cb74c0186 i18n: use example.com in email placeholder strings 2026-07-09 14:54:04 +02:00
dealerwebandLinus Rath c0515001f1 i18n: restore key parity after the Jalali calendar
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.
2026-07-08 22:43:49 +02:00
Hamed FallahandGitHub e10fced28a feat: add Jalali (Persian/Shamsi) calendar support with Saturday as week start (#490)
* 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
2026-07-08 15:42:42 +02:00
dealerwebandLinus Rath 3d36492518 i18n: restore key parity after the Hebrew locale
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.
2026-07-08 15:42:02 +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
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
dealerwebandLinus Rath a4dc0b7b4e i18n: restore key parity for the Slovak locale
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.
2026-07-06 13:36:51 +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 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
honzupandLinus Rath 14c2807f0a feat: add user-selectable regional date format 2026-07-04 14:53:37 +02:00
Linus Rath d4c066622b i18n: add missing translation keys across 19 locales 2026-06-28 20:35:23 +02:00
Linus Rath de68d68fb7 feat: add "New address book" creation UI #415 2026-06-24 19:53:41 +02:00
Linus Rath 84b6d0fd8b i18n: add missing translation keys across 19 locales 2026-06-24 19:36:55 +02:00
Linus Rath 32f0a67dbc fix: gate Send-now toolbar label, translate send_now across locales 2026-06-24 16:52:36 +02:00
hamedf62andLinus Rath f1b9f4ba50 feat(i18n): complete Farsi (fa) translation - 2654 translated strings
- Comprehensive Persian translation of all 2705 locale keys
- Covers login, sidebar, email viewer, composer, settings,
  calendar, contacts, files, S/MIME, tour, and all other sections
- 51 keys intentionally match English (language names, placeholders, templates)
- 98.1% of all strings fully translated to Persian
2026-06-24 16:34:27 +02:00
hamedf62andLinus Rath cc20d29636 feat: add Farsi (fa) locale support
Add comprehensive Farsi translation for the webmail interface:
- Create locales/fa/common.json with Farsi translations
- Register 'fa' locale in i18n/routing.ts and i18n/request.ts
- Add Iran flag component (FlagIR) to flag-icons.tsx
- Add ف��رسی to language switcher dropdown
- Add Farsi language name to English locale for language selector

Translation covers login, sidebar, email viewer, composer, settings,
notifications, calendar, contacts, errors, shortcuts, and more.
2026-06-24 16:34:27 +02:00