mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-30 16:16:19 +00:00
59 lines
2.3 KiB
TypeScript
59 lines
2.3 KiB
TypeScript
// "Before" hooks whose handlers may either stop an action (return false) or
|
|
// change what it does (return a value), such as onBeforeExternalLink
|
|
// (`{ href }`) and onBeforeCompose (a ComposeOptions to prefill). A payload
|
|
// changed in place has no effect: plugin handlers run in a sandboxed iframe
|
|
// and receive a copy.
|
|
|
|
import { uiHooks, type HookBus } from './plugin-hooks';
|
|
import type { ExternalLinkContext } from './plugin-types';
|
|
import { isOpenableLinkHref } from './email-sanitization';
|
|
|
|
export type InterceptResult<V> = { allowed: false } | { allowed: true; value: V };
|
|
|
|
/**
|
|
* Ask every handler of an intercept bus once, in order. `false` stops the
|
|
* action; any other return value goes through `accept`. See
|
|
* HookBus.interceptWithValue.
|
|
*/
|
|
export async function interceptWithResult<V>(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
bus: HookBus<any>,
|
|
value: V,
|
|
accept: (result: unknown, current: V) => V | undefined,
|
|
): Promise<InterceptResult<V>> {
|
|
if (!(bus && bus.size > 0)) return { allowed: true, value };
|
|
return bus.interceptWithValue(value, accept);
|
|
}
|
|
|
|
/**
|
|
* `accept` for onBeforeExternalLink: a handler may return `{ href }` to send
|
|
* the click elsewhere. Only a web address is taken, so a plugin can't turn a
|
|
* link into javascript:, data: or a path inside the app.
|
|
*/
|
|
export function acceptLinkRewrite<T extends { href: string }>(result: unknown, current: T): T | undefined {
|
|
if (!result || typeof result !== 'object') return undefined;
|
|
const href = (result as { href?: unknown }).href;
|
|
if (typeof href !== 'string') return undefined;
|
|
const trimmed = href.trim();
|
|
if (!/^https?:/i.test(trimmed)) return undefined;
|
|
try {
|
|
new URL(trimmed);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
return { ...current, href: trimmed };
|
|
}
|
|
|
|
/**
|
|
* Ask onBeforeExternalLink about a click on a link in a message. Resolves to
|
|
* the address to open: the original, the one a plugin rewrote it to, or null
|
|
* when a plugin stopped it.
|
|
*/
|
|
export async function resolveExternalLink(ctx: ExternalLinkContext): Promise<string | null> {
|
|
const outcome = uiHooks.onBeforeExternalLink.size > 0
|
|
? await uiHooks.onBeforeExternalLink.interceptWithValue(ctx, acceptLinkRewrite)
|
|
: { allowed: true as const, value: ctx };
|
|
if (!outcome.allowed || !isOpenableLinkHref(outcome.value.href)) return null;
|
|
return outcome.value.href;
|
|
}
|