mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 08:15:41 +00:00
144 lines
6.1 KiB
TypeScript
144 lines
6.1 KiB
TypeScript
"use client";
|
|
|
|
import { useSyncExternalStore } from 'react';
|
|
import { noThirdPartyTraffic, subscribeThirdPartyMode } from '@/lib/third-party-mode';
|
|
import type { SettingsPolicy } from '@/lib/admin/types';
|
|
import { accountSubjectPending, subjectForAccount, useAccountSubjects } from '@/lib/org/account-subjects';
|
|
import { usePolicyStore } from '@/stores/policy-store';
|
|
import { useAuthStore } from '@/stores/auth-store';
|
|
|
|
/**
|
|
* Whether the browser must stay off built-in outside hosts (#903), as React
|
|
* state: components re-render when the config arrives. True while the config
|
|
* is still unknown, so nothing reaches out before it says it may.
|
|
*/
|
|
export function useNoThirdPartyTraffic(): boolean {
|
|
return useSyncExternalStore(subscribeThirdPartyMode, noThirdPartyTraffic, () => true);
|
|
}
|
|
|
|
type PolicyState = ReturnType<typeof usePolicyStore.getState>;
|
|
|
|
/**
|
|
* The effective policy of a signed-in account (AccountEntry id): the active
|
|
* one's from the store, another one's (a message of it in the unified
|
|
* inbox or a Pro tab) resolved for its own domain and group scopes.
|
|
*/
|
|
function policyForAccount(state: PolicyState, accountId: string | null | undefined): SettingsPolicy {
|
|
const subject = subjectForAccount(accountId ?? null);
|
|
return subject === state.subject ? state.policy : state.resolveFor(subject);
|
|
}
|
|
|
|
/**
|
|
* The external content setting locked to "block" by the organisation, or
|
|
* limited to "block" as its only allowed value.
|
|
*/
|
|
function externalContentLockedToBlock(policy: SettingsPolicy): boolean {
|
|
const restriction = policy.restrictions?.externalContentPolicy;
|
|
const allowed = restriction?.allowedValues;
|
|
if (Array.isArray(allowed) && allowed.length > 0 && !allowed.includes('allow') && !allowed.includes('ask')) return true;
|
|
if (restriction?.locked !== true) return false;
|
|
// A lock without a value locks the organisation's default (as the
|
|
// settings store applies it).
|
|
const value = Object.prototype.hasOwnProperty.call(restriction, 'value')
|
|
? restriction.value
|
|
: policy.defaults?.externalContentPolicy;
|
|
return value === 'block';
|
|
}
|
|
|
|
/** Another signed-in account than the active one. */
|
|
function isOtherAccount(accountId: string | null | undefined): accountId is string {
|
|
const auth = useAuthStore.getState();
|
|
return !!accountId && accountId !== auth.activeAccountId && !!auth.getClientForAccount?.(accountId);
|
|
}
|
|
|
|
/**
|
|
* Whether the policy that may lock remote content for `accountId` isn't
|
|
* known yet: the first fetch hasn't answered, only the public subset is in
|
|
* (just after sign-in), or the policy has domain or group scopes and the
|
|
* account's groups are still being worked out. A later refetch keeps the
|
|
* policy it already has, so it doesn't count.
|
|
*/
|
|
function policyPending(state: PolicyState, accountId: string | null | undefined): boolean {
|
|
if (state.fetching && !state.loaded) return true;
|
|
if (state.partial) return true;
|
|
if ((state.rawPolicy.scopes ?? []).length === 0) return false;
|
|
return isOtherAccount(accountId) ? accountSubjectPending(accountId) : state.subjectPending;
|
|
}
|
|
|
|
/** Re-renders on anything the remote-content decisions read. */
|
|
function usePolicyInputs(): void {
|
|
useSyncExternalStore(subscribeThirdPartyMode, noThirdPartyTraffic, () => true);
|
|
usePolicyStore((s) => s.policy);
|
|
usePolicyStore((s) => s.rawPolicy);
|
|
usePolicyStore((s) => s.subject);
|
|
usePolicyStore((s) => s.subjectPending);
|
|
usePolicyStore((s) => s.fetching);
|
|
usePolicyStore((s) => s.loaded);
|
|
usePolicyStore((s) => s.partial);
|
|
useAccountSubjects((s) => s.subjects);
|
|
useAccountSubjects((s) => s.settled);
|
|
useAuthStore((s) => s.activeAccountId);
|
|
}
|
|
|
|
/**
|
|
* Whether the organisation keeps remote content in mail from loading at all
|
|
* for the account a message belongs to (the active one when left out): no
|
|
* third-party traffic, or the external content setting locked to "block" in
|
|
* that account's policy. Then neither a one-off load nor a trusted sender
|
|
* lets it through.
|
|
*/
|
|
export function remoteContentBlockedByOrg(accountId?: string | null): boolean {
|
|
return noThirdPartyTraffic() || externalContentLockedToBlock(policyForAccount(usePolicyStore.getState(), accountId));
|
|
}
|
|
|
|
/**
|
|
* Whether remote content stays blocked whatever the user chose: blocked by
|
|
* the organisation, or the policy that may lock it isn't known yet. Until
|
|
* it is, neither a trusted sender nor a one-off load lets anything through,
|
|
* so the first paint can't fire a tracker the organisation blocks.
|
|
*/
|
|
export function remoteContentHeldBack(accountId?: string | null): boolean {
|
|
return remoteContentBlockedByOrg(accountId) || policyPending(usePolicyStore.getState(), accountId);
|
|
}
|
|
|
|
/**
|
|
* remoteContentHeldBack for every signed-in account: for content whose
|
|
* account isn't known (a re-opened draft's quote, which may come from any).
|
|
*/
|
|
export function remoteContentHeldBackAnywhere(): boolean {
|
|
const connected = useAuthStore.getState().getAllConnectedClients?.();
|
|
const ids = connected ? [...connected.keys()] : [];
|
|
return remoteContentHeldBack() || ids.some((id) => remoteContentHeldBack(id));
|
|
}
|
|
|
|
/** remoteContentHeldBackAnywhere as React state (attachment previews, whose account isn't known). */
|
|
export function useRemoteContentHeldBackAnywhere(): boolean {
|
|
usePolicyInputs();
|
|
useAuthStore((s) => s.connectedAccountsRevision);
|
|
return remoteContentHeldBackAnywhere();
|
|
}
|
|
|
|
/** remoteContentBlockedByOrg as React state, for the account a message belongs to. */
|
|
export function useRemoteContentBlockedByOrg(accountId?: string | null): boolean {
|
|
usePolicyInputs();
|
|
return remoteContentBlockedByOrg(accountId);
|
|
}
|
|
|
|
/** remoteContentHeldBack as React state, for the account a message belongs to. */
|
|
export function useRemoteContentHeldBack(accountId?: string | null): boolean {
|
|
usePolicyInputs();
|
|
return remoteContentHeldBack(accountId);
|
|
}
|
|
|
|
/**
|
|
* For views with messages of several accounts (a conversation): re-renders
|
|
* on what the decisions read and hands back the per-account checks.
|
|
*/
|
|
export function useRemoteContentChecks(): {
|
|
heldBack: (accountId?: string | null) => boolean;
|
|
blockedByOrg: (accountId?: string | null) => boolean;
|
|
} {
|
|
usePolicyInputs();
|
|
return { heldBack: remoteContentHeldBack, blockedByOrg: remoteContentBlockedByOrg };
|
|
}
|