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.
This commit is contained in:
Linus Rath
2026-09-24 19:03:34 +02:00
parent eca3871545
commit 95c24a4214
2 changed files with 38 additions and 1 deletions
@@ -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);
});
});
+17 -1
View File
@@ -42,6 +42,7 @@ const EMAIL_PROPERTIES = [
interface PushPersistedSettings {
emailNotificationsEnabled?: boolean;
senderFavicons?: boolean;
}
async function emailNotificationsAllowed(): Promise<boolean> {
@@ -55,6 +56,20 @@ async function emailNotificationsAllowed(): Promise<boolean> {
}
}
// 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<boolean> {
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({