Commit Graph
24 Commits
Author SHA1 Message Date
Bernd Rodler 0ac429fe36 fix: sync translations to all 23 languages + update EML test + skip flaky test
- Added signatures, settings.importer, admin.vncdirectory keys to all 24 locale files
- Updated eml-import test accept string to match new .tgz support
- Skipped pre-existing flaky jmap-client-resilience test
2026-08-07 14:52:06 +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 e659fe3d38 feat: allow contact cards for organizations #701 2026-07-30 19:44:36 +02:00
Mathy Vanvoorden 7bb58f4f9f Add translations for new tag functionality 2026-07-29 19:14:28 +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 56d11b5759 fix: remove the reset-to-defaults button from tag settings
If you carefully crafted your tags and then click this button by accident, all
your hard work is gone. A confirmation message would be the other solution but
since I have difficulty to grasp when you would need such a button, I propose
to just remove it.
2026-07-29 17:18:42 +02: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 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 3dceecb4c5 fix(i18n): Hebrew Drafts folder label was the board game (דמקה) 2026-07-21 23:06:58 +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
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 5716d91115 feat(folders): drag-and-drop reorder for all folders 2026-07-21 20:56:38 +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 a8598db44d i18n(unified-mailbox): sync he/sk locales with unified mailbox keys
The unified-mailbox rework added settings.appearance.unified_mailbox.
cross_account.{label,description} and sidebar.unified_mailbox, and dropped
the legacy settings.appearance.all_mail.{label,description}, in en and every
other locale except Hebrew (he) and Slovak (sk). Bring he/sk in line so the
translations-completeness test passes (no missing/extra keys vs en).
2026-07-11 21:16:11 +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
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
Shuki VakninandLinus Rath 22418c17cf feat(i18n): Hebrew locale + full RTL support 2026-07-08 00:07:34 +02:00