mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 08:15:38 +00:00
fix: let identity hooks stop a plugin's change, send notification clicks to plugins that show notifications, and mark reading mail as high risk
This commit is contained in:
@@ -69,7 +69,9 @@ export function PluginAppEvents() {
|
||||
const onSwMessage = (e: MessageEvent) => {
|
||||
const msg = e.data as { kind?: string; tag?: string; data?: unknown } | null;
|
||||
if (msg && msg.kind === 'notificationclick' && typeof msg.tag === 'string') {
|
||||
void toastHooks.onNotificationClick.emit({ tag: msg.tag, data: msg.data });
|
||||
// App notifications (new mail, reminders): the tag only. Their data
|
||||
// names messages and events, which this hook's permission doesn't cover.
|
||||
void toastHooks.onNotificationClick.emit({ tag: msg.tag });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTemplateStore } from '@/stores/template-store';
|
||||
import { useIdentityStore } from '@/stores/identity-store';
|
||||
import { useVacationStore } from '@/stores/vacation-store';
|
||||
import { HIGH_RISK_PERMISSIONS } from '../plugin-sandbox/permission-text';
|
||||
import { clearAllHooks, identityHooks } from '../plugin-hooks';
|
||||
|
||||
vi.mock('@/stores/toast-store', () => ({
|
||||
toast: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() },
|
||||
@@ -132,6 +133,16 @@ describe('api.identities.update', () => {
|
||||
expect(client.updateIdentity).not.toHaveBeenCalled();
|
||||
expect(HIGH_RISK_PERMISSIONS.has('identity:write')).toBe(true);
|
||||
});
|
||||
|
||||
it('lets another plugin stop the change, like one made in Settings', async () => {
|
||||
identityHooks.onBeforeIdentityChange.register('guard', () => false);
|
||||
try {
|
||||
await expect(call(ALL, 'identities.update', 'id1', { name: 'x' })).resolves.toBeNull();
|
||||
expect(client.updateIdentity).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
clearAllHooks();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('api.vacation', () => {
|
||||
|
||||
@@ -270,7 +270,7 @@ export const HOOK_CATALOG = {
|
||||
// ─── Notifications ─────────────────────────────────────────
|
||||
onToastShow: H('notifications', 'observer', 'ui:observe', '{ id, type, title, message }', 'when the app shows a toast'),
|
||||
onToastDismiss: H('notifications', 'observer', 'ui:observe', '{ id }', 'when a toast is dismissed'),
|
||||
onNotificationClick: H('notifications', 'observer', 'email:read', '{ tag, data }', 'when a system notification is clicked'),
|
||||
onNotificationClick: H('notifications', 'observer', 'ui:notifications', '{ tag }', 'when a notification the plugin showed, or one from the app, is clicked'),
|
||||
|
||||
// ─── App lifecycle ─────────────────────────────────────────
|
||||
onAppReady: H('app', 'observer', 'app:lifecycle', '{ accountId }', 'once after the app has started and the mail data is loaded'),
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
} from '../plugin-types';
|
||||
import type { EmailAddress, Identity } from '../jmap/types';
|
||||
import { useTemplateStore } from '@/stores/template-store';
|
||||
import { useIdentityStore } from '@/stores/identity-store';
|
||||
import { updateIdentityWithHooks, useIdentityStore } from '@/stores/identity-store';
|
||||
import { useVacationStore } from '@/stores/vacation-store';
|
||||
import { identityToView, templateToView, vacationToView } from '../plugin-projection';
|
||||
import { sanitizeEmailHtml, sanitizePluginBodyHtml, sanitizeSignatureHtml } from '../email-sanitization';
|
||||
@@ -112,7 +112,7 @@ const MAX_HTML_SIGNATURE = 100_000;
|
||||
* address itself can't be changed; an HTML signature is sanitised exactly
|
||||
* like one typed into Settings > Identities.
|
||||
*/
|
||||
export async function doIdentitiesUpdate(id: unknown, patch: unknown): Promise<IdentityView> {
|
||||
export async function doIdentitiesUpdate(id: unknown, patch: unknown): Promise<IdentityView | null> {
|
||||
const method = 'identities.update';
|
||||
const identityId = readId(id, method);
|
||||
if (patch && typeof patch === 'object' && 'email' in patch) {
|
||||
@@ -141,7 +141,10 @@ export async function doIdentitiesUpdate(id: unknown, patch: unknown): Promise<I
|
||||
updates.htmlSignature = html.trim() ? sanitizeSignatureHtml(html) : null;
|
||||
}
|
||||
|
||||
await sessionClient(method).updateIdentity(identityId, updates);
|
||||
// Through the store's hooked update, so onBeforeIdentityChange can stop a
|
||||
// plugin's change like it stops one made in Settings.
|
||||
const done = await updateIdentityWithHooks(sessionClient(method), identity as Identity, updates as Parameters<typeof updateIdentityWithHooks>[2]);
|
||||
if (!done) return null;
|
||||
useIdentityStore.getState().updateIdentityLocal(identityId, updates as Partial<Identity>);
|
||||
return identityToView({ ...identity, ...updates } as Identity);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import { ALL_PERMISSIONS, IMPLICIT_PERMISSIONS } from '../plugin-types';
|
||||
*/
|
||||
export const HIGH_RISK_PERMISSIONS: ReadonlySet<string> = new Set([
|
||||
'crypto:full',
|
||||
// email.get reads any message in the account, not only the open one.
|
||||
'email:read',
|
||||
'email:send',
|
||||
'email:raw-send',
|
||||
'email:blob-read',
|
||||
|
||||
@@ -693,10 +693,11 @@ function buildPluginApi(manifest: PluginManifest) {
|
||||
/**
|
||||
* Change the name, reply-to addresses or signatures of one identity.
|
||||
* The address can't be changed. An HTML signature is sanitised like
|
||||
* one typed into Settings. Resolves to the identity as changed.
|
||||
* one typed into Settings. Resolves to the identity as changed, or null
|
||||
* when an onBeforeIdentityChange handler stopped it.
|
||||
*/
|
||||
update: (id: string, patch: IdentityPatch) =>
|
||||
callApi('identities.update', [id, patch]) as Promise<IdentityView>,
|
||||
callApi('identities.update', [id, patch]) as Promise<IdentityView | null>,
|
||||
},
|
||||
// Out-of-office reply (permissions: vacation:read / vacation:write).
|
||||
vacation: {
|
||||
|
||||
Reference in New Issue
Block a user