From 809db3b38d2e7bf3ddc02573191f9765202bee0e Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sat, 26 Sep 2026 14:45:20 +0200
Subject: [PATCH] fix: block remote content in the mobile conversation view
The conversation view used on phones built its own sanitizer hook that
only caught `` starting with a lowercase scheme and inline
style `url()`, and its iframe CSP always allowed http(s) images. With
remote content blocked (the default `ask` policy) uppercase schemes,
srcset, ``, video posters and background attributes still
loaded, so trackers fired when a phone user opened a thread.
The view now uses the desktop viewer's `blockExternalResourcesOnNode`,
and the strict iframe CSP is shared as `emailIframeCsp()` and applied
whenever remote content is blocked, together with `no-referrer`.
---
...ad-conversation-view-remote-block.test.tsx | 54 +++++++++++++++++++
components/email/email-viewer.tsx | 19 ++-----
components/email/thread-conversation-view.tsx | 36 +++++--------
lib/email-sanitization.ts | 16 ++++++
4 files changed, 89 insertions(+), 36 deletions(-)
create mode 100644 components/email/__tests__/thread-conversation-view-remote-block.test.tsx
diff --git a/components/email/__tests__/thread-conversation-view-remote-block.test.tsx b/components/email/__tests__/thread-conversation-view-remote-block.test.tsx
new file mode 100644
index 000000000..5b4ed7ea5
--- /dev/null
+++ b/components/email/__tests__/thread-conversation-view-remote-block.test.tsx
@@ -0,0 +1,54 @@
+// The mobile conversation view built its own sanitizer hook, which caught
+// only and inline style url(), and a CSP that always allowed
+// http:/https: images - so with remote content blocked (default policy
+// 'ask') srcset, background attributes, posters and still loaded.
+import { describe, expect, it, vi } from 'vitest';
+import { render } from '@testing-library/react';
+
+const t = Object.assign((k: string) => k, { raw: (k: string) => k, rich: (k: string) => k });
+vi.mock('next-intl', () => ({
+ useTranslations: () => t,
+ useLocale: () => 'en',
+}));
+vi.mock('@/hooks/use-wopi-status', () => ({ useWopiStatus: () => ({ enabled: false }), canWopiOpen: () => false }));
+vi.mock('@/hooks/use-own-domain-address', () => ({ useOwnDomainAddress: () => () => false }));
+
+const TRACKERS = ['upper.gif', 'srcset.gif', 'bgattr.gif', 'poster.gif', 'source.gif'];
+const HTML = `
+