Mail Server Method Registry
Shared per-method dispatch registry for the IMAP / JMAP / ManageSieve listener factories. Replaces the hand-rolled switch (verb) dispatchers with a single primitive that:
- runs the protocol-specific guard chain BEFORE the handler lookup (so operator-supplied overrides cannot bypass b.guardImapCommand / b.guardJmap / b.guardManagesieveCommand), - requires a per-handler resource budget (maxHandlerBytes, maxHandlerMs) and refuses a registration that omits either. Dispatch applies maxHandlerMs: a handler past it is answered for, and the listener then ends the connection. maxHandlerBytes is carried on the entry for the listener that owns the socket to read; dispatch does not count bytes, because a handler writes to the socket directly and the registry never sees them, - emits a mail.server_registry.method_dispatch audit event with handler-source (builtin | operator-override), - rejects registrations of method names outside the per-protocol IANA / RFC catalog (unless allowExperimental: true, which itself audits).
Operators wanting to override IMAP FETCH, JMAP Email/query, ManageSieve PUTSCRIPT, etc. supply opts.overrides: { "FETCH": { fn, maxHandlerBytes, maxHandlerMs } } to the listener factory and the registry routes dispatch through the override without touching wire-protocol state, audit lifecycle, or the guard substrate.
Per-handler resource budgets are required (no auto-defaults) per the framework's security-defaults-on rule: an operator-supplied FETCH override that omits a budget could regress CVE-2024-34055 (Cyrus authenticated OOM via unbounded allocation); the stricter-mode contract forces the operator to acknowledge the resource ceiling explicitly.
b.mail.serverRegistry.jmapMethodTakesAccount(name) #
Report whether a JMAP method the registry knows takes an accountId. RFC 8620 section 1.6.2 gives most methods a mandatory accountId, and Core/echo is the one method in the registry's RFC 8620 and RFC 8621 list defined without one. b.mail.server.jmap asks this before it dispatches, and answers invalidArguments when an account-scoped method names no account. A name the registry does not know reads false: an experimental method declares its own arguments.
b.mail.serverRegistry.jmapMethodTakesAccount("Email/get"); // → true
b.mail.serverRegistry.jmapMethodTakesAccount("Core/echo"); // → false
b.mail.serverRegistry.jmapMethodIsAccountFree(name) #
Report whether this is a method the registry knows to take no account at all. Core/echo is the one such method in the registry's RFC 8620 and RFC 8621 list, and RFC 8620 section 4 returns its arguments unchanged, so a member of them named like an account is data rather than a target.
This is not the negation of jmapMethodTakesAccount, which also reads false for a name the registry has never heard of. An operator's own method is not known to be account-free, so it keeps every check.
b.mail.serverRegistry.jmapMethodIsAccountFree("Core/echo"); // → true
b.mail.serverRegistry.jmapMethodIsAccountFree("Custom/run"); // → false
b.mail.serverRegistry.create(opts) #
{
protocol: "imap" | "jmap" | "managesieve",
defaults: { [name]: { fn, maxHandlerBytes, maxHandlerMs } },
overrides: { [name]: { fn, maxHandlerBytes, maxHandlerMs } },
notFoundHandler: function (name, ctx), // optional; returns the protocol's "not configured" reply
}
Build a per-method dispatch registry for one of the mail-server listeners. Returns { register, unregister, dispatch, list, has, source, MailServerRegistryError }.
var reg = b.mail.serverRegistry.create({
protocol: "imap",
defaults: { CAPABILITY: { fn: _capabilityHandler,
maxHandlerBytes: 8 * 1024,
maxHandlerMs: 5 * 1000 } },
overrides: opts.overrides || {},
});
await reg.dispatch("CAPABILITY", state, socket, parsed);