mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: look up another signed-in account for the external mail checks only when an admin turned them on
This commit is contained in:
@@ -31,7 +31,7 @@ function useAccountContext(accountId: string | null | undefined): ExternalMailCo
|
||||
useEffect(() => {
|
||||
if (!other || !accountId) return;
|
||||
let cancelled = false;
|
||||
void externalMailContextFor(accountId).then((context) => {
|
||||
void externalMailContextFor(accountId, 'senderLabel').then((context) => {
|
||||
if (!cancelled) setOtherContext({ accountId, context });
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
|
||||
@@ -38,7 +38,10 @@ vi.mock('@/components/plugins/plugin-slot', () => ({ PluginSlot: () => null }));
|
||||
vi.mock('@/hooks/use-plugin-slot-offers', () => ({ usePluginSlotOffers: () => [] }));
|
||||
vi.mock('@/hooks/use-config', () => ({ useConfig: () => ({ appName: 'Mail' }) }));
|
||||
vi.mock('@/hooks/use-identity-sync', () => ({ useIdentitySync: () => {} }));
|
||||
vi.mock('@/hooks/use-pro-multi-account-identities', () => ({ useProMultiAccountIdentities: () => [] }));
|
||||
vi.mock('@/hooks/use-pro-multi-account-identities', () => ({
|
||||
useProMultiAccountIdentities: () => [],
|
||||
cachedIdentitiesForAccount: () => undefined,
|
||||
}));
|
||||
vi.mock('@/hooks/use-pro-multi-account-mailboxes', () => ({ useProMultiAccountMailboxes: () => {} }));
|
||||
vi.mock('@/hooks/use-wopi-status', () => ({ useWopiStatus: () => null, canWopiOpen: () => false }));
|
||||
vi.mock('@/hooks/use-browser-navigation', () => ({ useBrowserNavigation: () => {} }));
|
||||
|
||||
@@ -185,6 +185,37 @@ describe('the active account', () => {
|
||||
expect((await externalMailContextFor(undefined)).policy.senderLabel).toBe(true);
|
||||
});
|
||||
|
||||
it("looks another account up only when the switch can be on for it", async () => {
|
||||
const calls = { subject: 0, identities: 0 };
|
||||
const eveClient = {
|
||||
getUsername: () => { calls.subject++; return 'org-eve@org-three.test'; },
|
||||
getSharedAccounts: () => [],
|
||||
getAccountInfo: () => null,
|
||||
getPrincipalTypes: async () => ({}),
|
||||
getIdentities: async () => { calls.identities++; return [identity('org-eve@org-three.test')]; },
|
||||
};
|
||||
useAuthStore.setState({
|
||||
identities: [identity('org-alice@example.org')],
|
||||
getClientForAccount: (id: string) => (id === 'acc-eve' ? eveClient : undefined),
|
||||
} as never);
|
||||
const scope = (externalMail: Record<string, boolean>) => ({ id: 's', label: 'S', match: { domains: ['org-three.test'] }, policy: { organisation: { externalMail } } });
|
||||
|
||||
// Off for Everyone and in every scope: no request at all.
|
||||
usePolicyStore.setState({ rawPolicy: { ...DEFAULT_POLICY, scopes: [scope({ recipientWarning: true })] } });
|
||||
expect((await externalMailContextFor('acc-eve', 'senderLabel')).policy.senderLabel).toBe(false);
|
||||
expect(calls).toEqual({ subject: 0, identities: 0 });
|
||||
|
||||
// On in a scope that doesn't match eve: her subject is needed, her identities aren't.
|
||||
usePolicyStore.setState({ rawPolicy: { ...DEFAULT_POLICY, scopes: [{ ...scope({ senderLabel: true }), match: { domains: ['elsewhere.test'] } }] } });
|
||||
expect((await externalMailContextFor('acc-eve', 'senderLabel')).identityEmails).toEqual([]);
|
||||
expect(calls).toEqual({ subject: 1, identities: 0 });
|
||||
|
||||
// On for her domain: her identities count.
|
||||
usePolicyStore.setState({ rawPolicy: { ...DEFAULT_POLICY, scopes: [scope({ senderLabel: true })] } });
|
||||
expect((await externalMailContextFor('acc-eve', 'senderLabel')).identityEmails).toEqual(['org-eve@org-three.test']);
|
||||
expect(calls.identities).toBe(1);
|
||||
});
|
||||
|
||||
it('collects the addresses of every contact', () => {
|
||||
useContactStore.setState({
|
||||
contacts: [
|
||||
|
||||
+20
-5
@@ -158,6 +158,13 @@ type AccountClient = NonNullable<ReturnType<ReturnType<typeof useAuthStore.getSt
|
||||
const subjects = new WeakMap<AccountClient, Promise<PolicySubject | null>>();
|
||||
const identityLists = new WeakMap<AccountClient, Promise<string[]>>();
|
||||
|
||||
/** Whether Everyone or any domain or group scope turns this switch on. */
|
||||
function switchOnAnywhere(key: 'senderLabel' | 'recipientWarning'): boolean {
|
||||
const raw = usePolicyStore.getState().rawPolicy;
|
||||
if (raw.organisation?.externalMail?.[key]) return true;
|
||||
return (raw.scopes ?? []).some((scope) => scope.policy.organisation?.externalMail?.[key] === true);
|
||||
}
|
||||
|
||||
/** Whether `localAccountId` names another signed-in account than the active one. */
|
||||
export function isOtherAccount(localAccountId: string | null | undefined): localAccountId is string {
|
||||
const auth = useAuthStore.getState();
|
||||
@@ -170,15 +177,26 @@ export function isOtherAccount(localAccountId: string | null | undefined): local
|
||||
* sent from. Its policy is resolved for its own subject, so a domain or
|
||||
* group scope of that account applies, and its own identities count. The
|
||||
* active account (or none given) reads the stores directly.
|
||||
*
|
||||
* With `need`, another account is only looked up when that switch can be on
|
||||
* for it: off everywhere in the policy costs no request, and off for that
|
||||
* account skips its identities.
|
||||
*/
|
||||
export async function externalMailContextFor(localAccountId?: string | null): Promise<ExternalMailContext> {
|
||||
export async function externalMailContextFor(
|
||||
localAccountId?: string | null,
|
||||
need?: 'senderLabel' | 'recipientWarning',
|
||||
): Promise<ExternalMailContext> {
|
||||
if (!isOtherAccount(localAccountId)) return activeExternalMailContext();
|
||||
if (need && !switchOnAnywhere(need)) return { policy: DEFAULT_ORGANISATION_POLICY.externalMail, identityEmails: [] };
|
||||
const client = useAuthStore.getState().getClientForAccount(localAccountId)!;
|
||||
let subject = subjects.get(client);
|
||||
if (!subject) {
|
||||
subject = policySubjectForClient(client).catch(() => null);
|
||||
subjects.set(client, subject);
|
||||
}
|
||||
const policy = usePolicyStore.getState().resolveFor(await subject).organisation?.externalMail
|
||||
?? DEFAULT_ORGANISATION_POLICY.externalMail;
|
||||
if (need && !policy[need]) return { policy, identityEmails: [] };
|
||||
let identities = identityLists.get(client);
|
||||
if (!identities) {
|
||||
const cached = cachedIdentitiesForAccount(localAccountId);
|
||||
@@ -187,10 +205,7 @@ export async function externalMailContextFor(localAccountId?: string | null): Pr
|
||||
: client.getIdentities().then((list) => list.map((identity) => identity.email)).catch(() => []);
|
||||
identityLists.set(client, identities);
|
||||
}
|
||||
const [resolvedSubject, identityEmails] = await Promise.all([subject, identities]);
|
||||
const policy = usePolicyStore.getState().resolveFor(resolvedSubject).organisation?.externalMail
|
||||
?? DEFAULT_ORGANISATION_POLICY.externalMail;
|
||||
return { policy, identityEmails };
|
||||
return { policy, identityEmails: await identities };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function ensureContactsLoaded(waitMs = CONTACTS_WAIT_MS): Promise<v
|
||||
export async function recipientsToConfirm(context: OutgoingContext): Promise<ExternalRecipient[]> {
|
||||
// The sending account's policy and identities: an identity of another
|
||||
// signed-in account sends under that account's domain or group scopes.
|
||||
const account = await externalMailContextFor(context.sender?.accountId);
|
||||
const account = await externalMailContextFor(context.sender?.accountId, 'recipientWarning');
|
||||
const { policy } = account;
|
||||
if (!policy.recipientWarning) return [];
|
||||
// The chosen identity's own address is internal. A From typed over it in
|
||||
|
||||
Reference in New Issue
Block a user