fix: look up another signed-in account for the external mail checks only when an admin turned them on

This commit is contained in:
Linus Rath
2026-10-28 08:07:55 +01:00
parent 9f7e875faf
commit 9304557819
5 changed files with 57 additions and 8 deletions
+1 -1
View File
@@ -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: () => {} }));
+31
View File
@@ -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
View File
@@ -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 };
}
/**
+1 -1
View File
@@ -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