mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 16:16:59 +00:00
onCalendarAlert now fires for a task's display alert too, as
{ id, taskId, taskTitle, triggerTime }, and closing that reminder's toast
fires onCalendarAlertAcknowledge with { alertId, taskId }. A task's
reminder reaches only plugins with tasks:read, an event's only plugins
with calendar:read; either permission registers the hooks, and the host
hands each call only to a plugin holding the one it needs.
onBeforeCalendarAlert, a new intercept, runs before Bulwark shows a
reminder of an event or a task, with the same payload. A plugin that
shows reminders itself returns false: Bulwark then plays no sound, sets
no title alert and shows no toast for that reminder, while
onCalendarAlert still fires. Like the reminder it runs in every open
tab, and each tab follows its own answer. It fails open within five
seconds, so a hung plugin delays a reminder but never drops it. Without
a handler the reminder shows at once, as before.
81 lines
3.7 KiB
TypeScript
81 lines
3.7 KiB
TypeScript
import type { InstalledPlugin, Permission, SlotName } from '../plugin-types';
|
|
import { IMPLICIT_PERMISSIONS } from '../plugin-types';
|
|
import { hookSpec } from '../plugin-hook-catalog';
|
|
import { slotSpec } from '../plugin-slot-catalog';
|
|
|
|
/**
|
|
* Whether `plugin` may use `perm`: the manifest must declare it, and an
|
|
* admin must have installed the plugin (managed) or the user must have
|
|
* granted it in the consent dialog. Implicit permissions always pass.
|
|
*/
|
|
export function pluginHasPermission(plugin: InstalledPlugin, perm: Permission): boolean {
|
|
if ((IMPLICIT_PERMISSIONS as readonly string[]).includes(perm)) return true;
|
|
if (!plugin.permissions.includes(perm)) return false;
|
|
// Defense-in-depth: even if the manifest declares a permission, the host
|
|
// refuses it unless an admin has marked the plugin as managed, or the user
|
|
// has explicitly granted it via the consent dialog.
|
|
if (plugin.managed) return true;
|
|
return (plugin.grantedPermissions ?? []).includes(perm);
|
|
}
|
|
|
|
// ─── Hooks ────────────────────────────────────────────────────
|
|
|
|
// The permission each hook needs comes from the hook catalogue
|
|
// (lib/plugin-hook-catalog.ts). A hook hands the plugin data (an opened
|
|
// message, the outgoing mail) or lets it change what the host does with it,
|
|
// so registering one is as much an API call as `email.get` and is gated the
|
|
// same way. A hook missing from the catalogue is refused.
|
|
|
|
export function hookPermission(hookName: string): Permission | null {
|
|
return hookSpec(hookName)?.permission ?? null;
|
|
}
|
|
|
|
export function mayRegisterHook(plugin: InstalledPlugin, hookName: string): boolean {
|
|
const spec = hookSpec(hookName);
|
|
if (!spec) return false;
|
|
return pluginHasPermission(plugin, spec.permission)
|
|
|| (spec.alsoPermission !== undefined && pluginHasPermission(plugin, spec.alsoPermission));
|
|
}
|
|
|
|
/**
|
|
* Whether one call of a hook may reach `plugin`: always for a hook with one
|
|
* permission (registering checked it), else when the plugin holds the
|
|
* permission this call's payload needs (a task's reminder: tasks:read).
|
|
*/
|
|
export function mayReceiveHookCall(plugin: InstalledPlugin, hookName: string, args: unknown[]): boolean {
|
|
const needs = hookSpec(hookName)?.callPermission;
|
|
return needs === undefined || pluginHasPermission(plugin, needs(args[0]));
|
|
}
|
|
|
|
// ─── Slots ────────────────────────────────────────────────────
|
|
|
|
// Which permissions let a plugin use each slot, and which props (or click
|
|
// context) it may see, come from the slot catalogue. A ui:* permission only
|
|
// lets a plugin draw somewhere; the message, event, contact or account the
|
|
// slot sits on is data, and stays behind the matching read permission. Props
|
|
// not listed in the catalogue are dropped, so a new prop at a call site is
|
|
// withheld until the catalogue names it.
|
|
|
|
export function slotPropsFor(
|
|
plugin: InstalledPlugin,
|
|
slot: SlotName,
|
|
props: Record<string, unknown>,
|
|
): Record<string, unknown> {
|
|
const allowed = slotSpec(slot)?.props;
|
|
if (allowed === '*') return props;
|
|
if (!allowed) return {};
|
|
const out: Record<string, unknown> = {};
|
|
for (const [key, value] of Object.entries(props)) {
|
|
if (!Object.prototype.hasOwnProperty.call(allowed, key)) continue;
|
|
const perm = allowed[key];
|
|
if (perm && !pluginHasPermission(plugin, perm)) continue;
|
|
out[key] = value;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function mayOfferSlot(plugin: InstalledPlugin, slot: string): boolean {
|
|
const perms = slotSpec(slot)?.permissions ?? null;
|
|
return !!perms && perms.some((perm) => pluginHasPermission(plugin, perm));
|
|
}
|