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.
The plugin runtime received the active locale (init payload + 'locale-change')
and plugins could declare a `locales` map, but none of it was usable: the
locales never reached the runtime, and buildPluginApi exposed no i18n. So
plugin code calling pluginApi.i18n.t(...) (as the External Link Warning plugin
does) always got undefined and fell back to English.
Thread plugin locales end to end and surface an i18n API:
- ServerPlugin gains `locales`; the upload route persists manifest.locales
(alongside configSchema/settingsSchema), and /api/plugins surfaces it to the
client so it flows registry -> client -> sandbox host-bridge -> runtime.
- runtime sets __PLUGIN_LOCALE__ at init (not only on later 'locale-change')
and buildPluginApi exposes `i18n.locale` + `i18n.t(key, vars)` resolving
against the plugin's declared locales (manifest.locales) with English/key
fallback and {placeholder} interpolation.
Lets any sandboxed plugin localize its strings from its manifest.