Files
webmail/lib/outgoing-hosts.ts

109 lines
3.6 KiB
TypeScript

/**
* 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));
}