Files
skills/docs/communication/emails/email-processing/src/hooks/hooks.md
2026-05-29 08:20:56 +01:00

3.1 KiB

hooks

(no submodules)

Purpose

Implements the package's email:processing hook pipeline: every parsed email passes through a sequence of handlers that may sanitise, redact, or enrich the parsed result. The implementation is intentionally local to this package — the foundation @suaveplan/events library does not (yet) export a shared Hooks<T> substrate, so a focused async pipeline lives here. The public shape matches the upstream @suaveplan/email-processing package so consumer code does not change.

Features

  • Generic Hooks<TMap> class with explicit type-safe hook / applyHooksAsync / clear methods.
  • Handlers run in order (default 100, lower first) and may be sync or async — applyHooksAsync awaits each return value.
  • Errors thrown from any handler short-circuit the pipeline and are wrapped in an EmailProcessingError with code EMAIL_PROCESSING.HOOK_FAILURE. The original throw is preserved on .cause when it is an Error.
  • Each invocation increments the email_processing.hook.invocation_total OTel counter with event and handlers attributes.
  • Returned data is threaded through every handler so transforms compose.
  • Module-level emailHooks singleton + onEmailProcessing shorthand.

Usage: Basic

import { onEmailProcessing } from "@suaveplan/email-processing/hooks";

onEmailProcessing((data) => ({
    ...data,
    parsed: {
        ...data.parsed,
        html: data.parsed.html?.replace(
            /<img[^>]*tracking[^>]*>/gi,
            ""
        ),
    },
}));

Usage: Advanced

import { Hooks, type EmailHookMap } from "@suaveplan/email-processing/hooks";

const hooks = new Hooks<EmailHookMap>();
const offFirst = hooks.hook(
    "email:processing",
    async (d) => {
        await audit.log({ messageId: d.parsed.messageId });
        return d;
    },
    { order: 10 }
);
const offRedact = hooks.hook(
    "email:processing",
    (d) => ({
        ...d,
        parsed: { ...d.parsed, text: redact(d.parsed.text ?? "") },
    }),
    { order: 200 }
);
try {
    const out = await hooks.applyHooksAsync("email:processing", incoming);
    return out;
} finally {
    offFirst();
    offRedact();
}

API Reference

  • EmailProcessingHookData — pipeline payload (parsed, rawInput?, metadata).
  • EmailHookMap — { "email:processing": EmailProcessingHookData }.
  • HookHandler<T> — (data: T) => T | Promise<T>.
  • HookHandlerOptions — { order?: number }.
  • Hooks<TMap> — class with hook, applyHooksAsync, clear.
  • emailHooks — singleton Hooks<EmailHookMap> instance.
  • onEmailProcessing(handler, options?) — shorthand registration on emailHooks; returns an idempotent unregister.

Implementation Notes

The unregister function returned from hook(...) is idempotent: calling it twice is a no-op so cleanup code in shared test fixtures can run without ordering pitfalls. The pipeline does not call the OTel counter when a handler throws — failures are observed via the EMAIL_PROCESSING.HOOK_FAILURE error path, and a separate consumer counter is the appropriate place to record those.