fix: give plugins no personal templates when the admin hid them

This commit is contained in:
Linus Rath
2026-10-28 07:34:23 +01:00
parent 63d64bba13
commit 4aa4606b84
2 changed files with 47 additions and 1 deletions
@@ -6,6 +6,9 @@ import { useAuthStore } from '@/stores/auth-store';
import { useTemplateStore } from '@/stores/template-store';
import { useIdentityStore } from '@/stores/identity-store';
import { useVacationStore } from '@/stores/vacation-store';
import { usePolicyStore } from '@/stores/policy-store';
import { emptyPartition, useAccountTemplatesStore } from '@/stores/account-templates-store';
import { DEFAULT_POLICY } from '../admin/types';
import { HIGH_RISK_PERMISSIONS } from '../plugin-sandbox/permission-text';
import { clearAllHooks, identityHooks } from '../plugin-hooks';
@@ -53,7 +56,9 @@ beforeEach(() => {
});
afterEach(() => {
useAuthStore.setState({ client: null });
useAuthStore.setState({ client: null, activeAccountId: null });
useAccountTemplatesStore.setState({ personal: {} });
usePolicyStore.setState({ policy: DEFAULT_POLICY });
useTemplateStore.setState({ templates: [], deletedTemplateIds: {} });
useIdentityStore.setState({ identities: [] });
useVacationStore.getState().clearState();
@@ -101,6 +106,33 @@ describe('api.templates', () => {
});
});
describe('api.templates and organisation settings', () => {
it('works on the account\'s own copy when the account keeps its templates (Stalwart 1.0)', async () => {
const inAccount = { ...greeting, id: 'acc-1', name: 'In the account' };
useAuthStore.setState({ activeAccountId: 'ann@mail' });
useAccountTemplatesStore.setState({ personal: { 'ann@mail': { ...emptyPartition(), templates: [inAccount] } } });
expect((await call(ALL, 'templates.list') as Array<{ name: string }>).map((t) => t.name)).toEqual(['In the account']);
const created = await call(ALL, 'templates.create', { name: 'From a plugin' }) as { id: string };
await call(ALL, 'templates.update', 'acc-1', { subject: 'Changed' });
await call(ALL, 'templates.delete', created.id);
const copy = useAccountTemplatesStore.getState().personal['ann@mail'];
expect(copy.templates.map((t) => t.subject)).toEqual(['Changed']);
expect(copy.deletedTemplateIds).toHaveProperty(created.id);
// The browser's list is left alone.
expect(useTemplateStore.getState().templates.map((t) => t.id)).toEqual(['tpl-1']);
});
it('shows and accepts nothing when the admin hid personal templates', async () => {
usePolicyStore.setState({ policy: { ...DEFAULT_POLICY, restrictions: { personalTemplates: { hidden: true } } } });
expect(await call(ALL, 'templates.list')).toEqual([]);
expect(await call(ALL, 'templates.get', 'tpl-1')).toBeNull();
await expect(call(ALL, 'templates.create', { name: 'x' })).rejects.toThrow(/turned personal templates off/);
await expect(call(ALL, 'templates.update', 'tpl-1', { name: 'x' })).rejects.toThrow(/turned personal templates off/);
await expect(call(ALL, 'templates.delete', 'tpl-1')).rejects.toThrow(/turned personal templates off/);
expect(useTemplateStore.getState().templates.map((t) => t.name)).toEqual(['Greeting']);
});
});
describe('api.identities.update', () => {
it('changes name, reply-to and signatures and never the address', async () => {
const view = await call(ALL, 'identities.update', 'id1', {
+14
View File
@@ -10,6 +10,7 @@ import type {
} from '../plugin-types';
import type { EmailAddress, Identity } from '../jmap/types';
import { personalTemplatesFor } from '@/lib/templates/personal-templates';
import { usePolicyStore } from '@/stores/policy-store';
import { updateIdentityWithHooks, useIdentityStore } from '@/stores/identity-store';
import { useVacationStore } from '@/stores/vacation-store';
import { identityToView, templateToView, vacationToView } from '../plugin-projection';
@@ -34,12 +35,22 @@ const TEMPLATE_FIELDS = ['name', 'subject', 'body', 'isHTML', 'category'] as con
// in the browser's list otherwise (lib/templates/personal-templates.ts).
const personal = () => personalTemplatesFor();
// The admin hid personal templates for the active account
// (`personalTemplates`): plugins see none and can't make any either.
const personalHidden = () => usePolicyStore.getState().policy.restrictions?.personalTemplates?.hidden === true;
function refuseIfHidden(method: string): void {
if (personalHidden()) throw new Error(`${method}: the administrator turned personal templates off`);
}
export function doTemplatesList(): TemplateView[] {
if (personalHidden()) return [];
return personal().templates().map(templateToView);
}
export function doTemplatesGet(id: unknown): TemplateView | null {
const templateId = readId(id, 'templates.get');
if (personalHidden()) return null;
const template = personal().templates().find((t) => t.id === templateId);
return template ? templateToView(template) : null;
}
@@ -64,6 +75,7 @@ export function doTemplatesCreate(input: unknown): TemplateView {
const method = 'templates.create';
const data = readObject(input, method, 'template', TEMPLATE_FIELDS) as TemplateInput & Record<string, unknown>;
if (data.name === undefined) throw new Error(`${method}: name is required`);
refuseIfHidden(method);
const store = personal();
if (store.templates().length >= MAX_PLUGIN_TEMPLATES) {
throw new Error(`${method}: there are already ${MAX_PLUGIN_TEMPLATES} templates`);
@@ -86,6 +98,7 @@ export function doTemplatesUpdate(id: unknown, patch: unknown): TemplateView {
const templateId = readId(id, method);
const data = readObject(patch, method, 'patch', TEMPLATE_FIELDS);
if (Object.keys(data).length === 0) throw new Error(`${method}: patch is empty`);
refuseIfHidden(method);
const store = personal();
const existing = store.templates().find((t) => t.id === templateId);
if (!existing) throw new Error(`${method}: no template with id "${templateId}"`);
@@ -100,6 +113,7 @@ export function doTemplatesUpdate(id: unknown, patch: unknown): TemplateView {
export function doTemplatesDelete(id: unknown): void {
const method = 'templates.delete';
const templateId = readId(id, method);
refuseIfHidden(method);
const store = personal();
if (!store.templates().some((t) => t.id === templateId)) {
throw new Error(`${method}: no template with id "${templateId}"`);