mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: give plugins no personal templates when the admin hid them
This commit is contained in:
@@ -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', {
|
||||
|
||||
@@ -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}"`);
|
||||
|
||||
Reference in New Issue
Block a user