From 43973608d612448030f1f7038b2c634f03338ca8 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sat, 31 Oct 2026 04:05:45 +0100
Subject: [PATCH] fix: show plain-text mail in the dark theme's own colours
instead of a fixed black
---
src/components/EmailBodyView.tsx | 10 ++++-
.../__tests__/plain-text-theme-colors.test.ts | 43 +++++++++++++++++++
src/lib/email-body-document.ts | 12 +++++-
src/lib/email-html.ts | 26 ++++++++---
4 files changed, 83 insertions(+), 8 deletions(-)
create mode 100644 src/lib/__tests__/plain-text-theme-colors.test.ts
diff --git a/src/components/EmailBodyView.tsx b/src/components/EmailBodyView.tsx
index 1082e9d9..78530d4d 100644
--- a/src/components/EmailBodyView.tsx
+++ b/src/components/EmailBodyView.tsx
@@ -688,6 +688,13 @@ export default function EmailBodyView({
// Sanitised and wrapped once per message, body and settings, and reused
// when the same body mounts again (lib/email-body-document).
const emptyLabel = t('email_viewer.no_body_content', '(No body content available)');
+ // A plain-text body in a dark app theme takes the theme's colours, not a
+ // fixed #0a0a0a. Only while the app itself is dark: a single message
+ // switched to dark in a light app has no dark palette to borrow.
+ const plainDarkColors = React.useMemo(
+ () => (resolvedTheme === 'dark' ? { background: c.background, text: c.text, link: c.textLink } : undefined),
+ [resolvedTheme, c.background, c.text, c.textLink],
+ );
const prepared = React.useMemo(() => bodyDocument({
key: `${jmapAccountId ?? ''}|${email.id}`,
rawHtml,
@@ -700,9 +707,10 @@ export default function EmailBodyView({
plainTextFont,
quoteLabels,
textFactor,
+ plainDarkColors,
}), [
jmapAccountId, email.id, rawHtml, text, email.preview, emptyLabel, shouldBlock, cidMap, renderAsDark,
- messageSpacing, plainTextFont, quoteLabels, textFactor,
+ messageSpacing, plainTextFont, quoteLabels, textFactor, plainDarkColors,
]);
const source = React.useMemo(() => ({ html: prepared.html }), [prepared.html]);
diff --git a/src/lib/__tests__/plain-text-theme-colors.test.ts b/src/lib/__tests__/plain-text-theme-colors.test.ts
new file mode 100644
index 00000000..6ee24b3a
--- /dev/null
+++ b/src/lib/__tests__/plain-text-theme-colors.test.ts
@@ -0,0 +1,43 @@
+import { describe, expect, it } from 'vitest';
+import fs from 'node:fs';
+import path from 'node:path';
+import { buildBodyDocument } from '../email-body-document';
+import { BUILTIN_THEMES } from '../../theme/builtin-themes';
+
+// A plain-text mail read in dark mode sat on a fixed #0a0a0a whatever the
+// app's theme was (UX audit 2026-10, "Plain-text mail dark background").
+
+const base = {
+ key: 'k', emptyLabel: '-', blockRemoteImages: false, cidMap: {}, rawHtml: null, text: 'hello',
+ messageSpacing: 'auto' as const, plainTextFont: 'sans' as const, quoteLabels: { show: 's', hide: 'h' },
+};
+
+describe('plain-text mail colours', () => {
+ it("takes the dark theme's ground, text and link colours", () => {
+ const nord = { background: '#2e3440', text: '#eceff4', link: '#88c0d0' };
+ const html = buildBodyDocument({ ...base, isDark: true, plainDarkColors: nord }).html;
+ expect(html).toContain('background: #2e3440;');
+ expect(html).toContain('color: #eceff4;');
+ expect(html).toContain('a { color: #88c0d0;');
+ expect(html).not.toContain('#0a0a0a');
+ });
+
+ it('keeps white in light mode and the old dark colours without a theme', () => {
+ const colors = { background: '#2e3440', text: '#eceff4', link: '#88c0d0' };
+ expect(buildBodyDocument({ ...base, isDark: false, plainDarkColors: colors }).html).toContain('background: #ffffff;');
+ expect(buildBodyDocument({ ...base, isDark: true }).html).toContain('background: #0a0a0a;');
+ });
+
+ it('cannot close the style sheet with a theme colour', () => {
+ const html = buildBodyDocument({
+ ...base, isDark: true, plainDarkColors: { background: 'red;}', text: '#fff', link: '#00f' },
+ }).html;
+ expect(html).not.toContain('