/** * The browser-side switches of the "no third-party traffic" mode (#903). * * Small on purpose: this module ships in every client chunk and in Lite. The * registry with the admin-facing descriptions is lib/admin/outgoing-hosts-registry.ts. * Every helper that opens an outside host asks noThirdPartyTraffic(), which is * true while the mode is on and while the config isn't known yet. */ import { noThirdPartyTraffic } from './third-party-mode'; /** Exact host names of the built-in outside hosts (the registry repeats them; a test keeps both equal). */ export const BUILT_IN_HOSTNAMES: readonly string[] = [ 'telemetry.bulwarkmail.org', 'version.telemetry.bulwarkmail.org', 'notifications.relay.bulwarkmail.org', 'extensions.bulwarkmail.org', 'connector.bulwarkmail.org', 'icons.duckduckgo.com', 'api.mymemory.translated.net', 'www.google.com', 'randomuser.me', 'img.freepik.com', ]; function hostnameOf(url: string): string | null { try { return new URL(url).hostname.toLowerCase(); } catch { return null; } } /** Whether a URL points at one of the built-in outside hosts. */ export function isBuiltInHost(url: string): boolean { const host = hostnameOf(url); return !!host && BUILT_IN_HOSTNAMES.includes(host); } /** A push relay may be used: always, except the built-in one in the mode. */ export function relayAllowed(url: string): boolean { return !(noThirdPartyTraffic() && isBuiltInHost(url)); } /** Sender icons through /api/favicon. */ export function senderFaviconsAllowed(): boolean { return !noThirdPartyTraffic(); } /** The Google Maps link on an event's address. */ export function mapsLinksAllowed(): boolean { return !noThirdPartyTraffic(); } /** OpenPGP WKD lookups at the recipient's domain. */ export function wkdAllowed(): boolean { return !noThirdPartyTraffic(); } /** Sample photos of the mock server. */ export function sampleAvatarsAllowed(): boolean { return !noThirdPartyTraffic(); } /** * An avatar URL may be shown: a plugin's (Gravatar, Libravatar...) or a * contact card's photo link. In the mode only when it stays on this origin or * is inline data, so those avatars fall back to initials instead of reaching * out (or tripping the CSP). */ export function avatarUrlAllowed(url: string | null | undefined, origin?: string): boolean { if (!url) return false; if (!noThirdPartyTraffic()) return true; if (/^(data|blob):/i.test(url)) return true; const base = origin ?? (typeof window !== 'undefined' ? window.location.origin : ''); try { return !!base && new URL(url, base).origin === new URL(base).origin; } catch { return false; } } export interface NetworkDeclaringPlugin { permissions?: readonly string[]; httpOrigins?: readonly string[]; frameOrigins?: readonly string[]; } /** * A plugin declares network access when it may fetch outside hosts * (`http:fetch` with httpOrigins), use the credential proxy (`http:proxy`) or * embed outside frames (frameOrigins). Same-origin `http:post` doesn't count. */ export function pluginDeclaresNetwork(plugin: NetworkDeclaringPlugin): boolean { const permissions = plugin.permissions ?? []; return permissions.includes('http:fetch') || permissions.includes('http:proxy') || (plugin.httpOrigins?.length ?? 0) > 0 || (plugin.frameOrigins?.length ?? 0) > 0; } /** Error code a refused plugin carries (shown translated in Settings > Plugins). */ export const PLUGIN_REFUSED_ERROR = 'third-party-off'; /** Whether a plugin may run in this browser. */ export function pluginAllowed(plugin: NetworkDeclaringPlugin): boolean { return !(noThirdPartyTraffic() && pluginDeclaresNetwork(plugin)); }