From 95c24a42147fcd1ad2366588b7a79f2f723e9caa Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 24 Sep 2026 18:43:22 +0200 Subject: [PATCH] fix: stop notifications fetching sender favicons when favicons are off The "Sender Favicons" setting only covered the in-app avatars. Push notifications always fetched the sender's favicon from icons.duckduckgo.com, so with the setting off every notification still sent the sender's domain and the device's IP to a third party. The webmail has the same setting, on by default, and fetches through its own `/api/favicon` proxy, so DuckDuckGo sees the server rather than the user. Native has no proxy, but turning the default off would silently drop the logos existing users see and diverge from the webmail, which is a product call. So the default stays on and the one existing switch now covers every favicon fetch: the notification task reads it from the persisted settings and shows initials when it is off, or when the settings cannot be read. --- .../__tests__/push-background-task.test.ts | 21 +++++++++++++++++++ src/lib/push-background-task.ts | 18 +++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/lib/__tests__/push-background-task.test.ts b/src/lib/__tests__/push-background-task.test.ts index b628ed9b..571973e0 100644 --- a/src/lib/__tests__/push-background-task.test.ts +++ b/src/lib/__tests__/push-background-task.test.ts @@ -10,10 +10,12 @@ vi.mock('../../api/jmap-client', () => ({ vi.mock('../client-cert', () => ({ secureFetch: vi.fn(async () => ({ ok: false, status: 500 })) })); vi.mock('../oauth', () => ({ refreshOAuthAccessToken: vi.fn(async (t: unknown) => t) })); +import AsyncStorage from '@react-native-async-storage/async-storage'; import { matchAccountsForPush, parseRelayPushData, selectNotifiableEmails, + senderFaviconsAllowed, } from '../push-background-task'; import type { Email } from '../../api/types'; @@ -88,3 +90,22 @@ describe('selectNotifiableEmails', () => { expect(out.map((e) => e.id)).toEqual(['a']); }); }); + +describe('senderFaviconsAllowed', () => { + const KEY = 'webmail:settings:v1'; + + it('follows the "Sender Favicons" setting the settings store persisted', async () => { + await AsyncStorage.setItem(KEY, JSON.stringify({ senderFavicons: false })); + expect(await senderFaviconsAllowed()).toBe(false); + await AsyncStorage.setItem(KEY, JSON.stringify({ senderFavicons: true })); + expect(await senderFaviconsAllowed()).toBe(true); + }); + + it('defaults to on like the store, but skips icons when the settings are unreadable', async () => { + await AsyncStorage.removeItem(KEY); + expect(await senderFaviconsAllowed()).toBe(true); + await AsyncStorage.setItem(KEY, '{not json'); + expect(await senderFaviconsAllowed()).toBe(false); + await AsyncStorage.removeItem(KEY); + }); +}); diff --git a/src/lib/push-background-task.ts b/src/lib/push-background-task.ts index 3e0b47a0..8ca29175 100644 --- a/src/lib/push-background-task.ts +++ b/src/lib/push-background-task.ts @@ -42,6 +42,7 @@ const EMAIL_PROPERTIES = [ interface PushPersistedSettings { emailNotificationsEnabled?: boolean; + senderFavicons?: boolean; } async function emailNotificationsAllowed(): Promise { @@ -55,6 +56,20 @@ async function emailNotificationsAllowed(): Promise { } } +// The "Sender Favicons" setting covers notification icons as well as the +// in-app avatars: with it off the tray shows initials, and the sender's domain +// and the device's IP never reach the icon service. Off when unreadable. +export async function senderFaviconsAllowed(): Promise { + try { + const raw = await AsyncStorage.getItem(SETTINGS_STORAGE_KEY); + if (!raw) return true; // first launch: the store's default is on + const parsed = JSON.parse(raw) as PushPersistedSettings; + return parsed.senderFavicons !== false; + } catch { + return false; + } +} + interface ShowNotificationOptions { notificationId: string; title: string; @@ -429,6 +444,7 @@ async function processAccountForPush(accountId: string, payload: RelayPushData): const groupKey = `bulwark-mail:${accountId}`; const groupTitle = payload.accountLabel ?? accountId.split('@')[0] ?? accountId; + const favicons = await senderFaviconsAllowed(); // Oldest first so the newest ends up on top of the tray. const ordered = [...toNotify].sort( @@ -442,7 +458,7 @@ async function processAccountForPush(accountId: string, payload: RelayPushData): const body = email.subject || '(no subject)'; const initials = getEmailInitials(name, address); const bgColorHex = hslToHex(generateEmailAvatarColor(name, address)); - const faviconDomain = getFaviconDomain(address); + const faviconDomain = favicons ? getFaviconDomain(address) : null; const iconUrl = faviconDomain ? getFaviconUrl(faviconDomain) : undefined; await native.showNotification({