Skip to content

Class: EmailPlugin

AppKit plugin that configures and verifies the SMTP transport used by the send_email tool, and exposes sending as an AppKit agent tool.

import { createApp, server } from "@databricks/appkit";
import { plugin as emailPlugin } from "@dbx-tools/email";
await createApp({
plugins: [
server(),
emailPlugin.email({
smtp: { host: "smtp.example.com", user: "apikey", password: process.env.SMTP_KEY },
domain: "mail.example.com",
}),
],
});
  • ToolProvider

new EmailPlugin(config): EmailPlugin

EmailPluginConfig

EmailPlugin

Plugin<EmailPluginConfig>.constructor

protected app: AppManager

Plugin.app


protected cache: CacheManager

Plugin.cache


protected config: EmailPluginConfig

Plugin.config


protected optional context?: PluginContext

Plugin.context


protected devFileReader: DevFileReader

Plugin.devFileReader


protected isReady: boolean

Plugin.isReady


name: string

Plugin name identifier.

Plugin.name


protected streamManager: StreamManager

Plugin.streamManager


protected telemetry: ITelemetry

Plugin.telemetry


static manifest: object

config: object

schema: JSONSchema7 = EMAIL_CONFIG_SCHEMA

description: string

displayName: string = "Email"

name: "email" = "email"

resources: object

optional: never[] = []

required: never[] = []

stability: "beta" = "beta"


static phase: PluginPhase

Plugin initialization phase.

  • ‘core’: Initialized first (e.g., config plugins)
  • ‘normal’: Initialized second (most plugins)
  • ‘deferred’: Initialized last (e.g., server plugin)

Plugin.phase

abortActiveOperations(): void

Abort in-flight work. AppKit’s graceful shutdown only invokes this hook - it never calls shutdown - so the SMTP pool is closed from here or it leaks at SIGTERM. The teardown is synchronous and idempotent, so the un-awaited call costs nothing.

void

Plugin.abortActiveOperations


asUser(req): this

Execute operations using the user’s identity from the request. Returns a proxy of this plugin where all method calls execute with the user’s Databricks credentials instead of the service principal.

Request

The Express request containing the user token in headers

this

A proxied plugin instance that executes as the user

AuthenticationError if user token is not available in request headers (production only). In development mode (NODE_ENV=development), skips user impersonation instead of throwing.

Plugin.asUser


attachContext(deps?): void

Binds runtime dependencies (telemetry provider, cache, plugin context) to this plugin. Called by AppKit._createApp after construction and before setup(). Idempotent: safe to call if the constructor already bound them eagerly. Kept separate so factories can eagerly construct plugin instances without running this before TelemetryManager.initialize() / CacheManager.getInstance() have run.

unknown

TelemetryOptions

void

Plugin.attachContext


clientConfig(): Record<string, unknown>

Returns startup config to expose to the client. Override this to surface server-side values that are safe to publish to the frontend, such as feature flags, resource IDs, or other app boot settings.

This runs once when the server starts, so it should not depend on request-scoped or user-specific state.

String values that match non-public environment variables are redacted unless you intentionally expose them via a matching PUBLIC_APPKIT_ env var.

Values must be JSON-serializable plain data (no functions, Dates, classes, Maps, Sets, BigInts, or circular references). By default returns an empty object (plugin contributes nothing to client config).

On the client, read the config with the usePluginClientConfig hook (React) or the getPluginClientConfig function (vanilla JS), both from @databricks/appkit-ui.

Record<string, unknown>

// Server — plugin definition
class MyPlugin extends Plugin<MyConfig> {
clientConfig() {
return {
warehouseId: this.config.warehouseId,
features: { darkMode: true },
};
}
}
// Client — React component
import { usePluginClientConfig } from "@databricks/appkit-ui/react";
interface MyPluginConfig { warehouseId: string; features: { darkMode: boolean } }
const config = usePluginClientConfig<MyPluginConfig>("myPlugin");
config.warehouseId; // "abc-123"
// Client — vanilla JS
import { getPluginClientConfig } from "@databricks/appkit-ui/js";
const config = getPluginClientConfig<MyPluginConfig>("myPlugin");

Plugin.clientConfig


protected execute<T>(fn, options, userKey?): Promise<ExecutionResult<T>>

Execute a function with the plugin’s interceptor chain.

Returns an ExecutionResult discriminated union:

  • { ok: true, data: T } on success
  • { ok: false, status: number, message: string } on failure

Errors are never thrown — the method is production-safe.

T

(signal?) => Promise<T>

PluginExecutionSettings

string

Promise<ExecutionResult<T>>

Plugin.execute


executeAgentTool(name, args, signal?): Promise<unknown>

AppKit ToolProvider: run one tool call. Arguments are validated against the tool’s schema first, and a validation failure comes back as an LLM-friendly string so the model can correct itself on the next turn.

string

unknown

AbortSignal

Promise<unknown>

ToolProvider.executeAgentTool


protected executeStream<T>(res, fn, options, userKey?): Promise<void>

T

IAppResponse

StreamExecuteHandler<T>

StreamExecutionSettings

string

Promise<void>

Plugin.executeStream


exports(): object

Returns the public exports for this plugin. Override this to define a custom public API. By default, returns an empty object.

The returned object becomes the plugin’s public API on the AppKit instance (e.g. appkit.myPlugin.method()). AppKit automatically binds method context and adds asUser(req) for user-scoped execution.

listSenders: () => Promise<{ defaultSender?: string; restricted: boolean; senders: string[]; }>

Sender options for the current user (the GET /senders payload). AppKit wraps this with asUser(req) for OBO scoping.

Promise<{ defaultSender?: string; restricted: boolean; senders: string[]; }>

sendEmail: (message, from, signal?, options?) => Promise<{ from: string; messageId?: string; recipient: string; sent: boolean; }>

Send a message immediately from from through the shared transport, bypassing the approval flow. For agent-driven sends use emailTool instead.

object[] = ...

string[] = ...

string = ...

string[] = ...

string = ...

string[] = ...

string

AbortSignal

SendEmailOptions

Promise<{ from: string; messageId?: string; recipient: string; sent: boolean; }>

class MyPlugin extends Plugin {
private getData() { return []; }
exports() {
return { getData: this.getData };
}
}
// After registration:
const appkit = await createApp({ plugins: [myPlugin()] });
appkit.myPlugin.getData();

Plugin.exports


getAgentTools(): AgentToolDefinition[]

AppKit ToolProvider: the tool definitions offered to an agent.

AgentToolDefinition[]

ToolProvider.getAgentTools


getEndpoints(): PluginEndpointMap

PluginEndpointMap

Plugin.getEndpoints


getSkipBodyParsingPaths(): ReadonlySet<string>

ReadonlySet<string>

Plugin.getSkipBodyParsingPaths


injectRoutes(router): void

Expose the sender-options lookup so UI compose views can populate a From dropdown from the configured allow-list. Mounted under the plugin base path, i.e. GET /api/email/senders. Runs in the OBO user scope so domain wildcards resolve against the caller’s own local part.

OBO is used only WHEN the request can support it. asUser(req) throws AuthenticationError outside NODE_ENV=development if the request carries no forwarded OBO token, and AppKit does not catch a rejection raised inside a handler - so unconditionally wrapping this route takes the process down for a caller that authenticated some other way (a @dbx-tools/tunnel OTP session, a health probe, a local curl). The user context is only ever an ENRICHMENT here: without it, wildcard senders simply expand against no local part. Degrading to the service context therefore answers correctly instead of failing, and a front-door request is unchanged.

This is the same rule @dbx-tools/appkit’s identity module applies in "auto" mode; the header check is inlined rather than taking a dependency on that package for one predicate.

Router

void

Plugin.injectRoutes


protected registerEndpoint(name, path): void

string

string

void

Plugin.registerEndpoint


protected resolveUserId(req): string

Resolve the effective user ID from a request.

Returns the x-forwarded-user header when present. In development mode (NODE_ENV=development) falls back to the current context user ID so that callers outside an active runInUserContext scope still get a consistent value.

Request

string

AuthenticationError in production when no user header is present.

Plugin.resolveUserId


protected route<_TResponse>(router, config): void

_TResponse

Router

RouteConfig

void

Plugin.route


setup(): Promise<void>

Prime the shared runtime from this plugin’s config (over env), route the tools’ sends through this plugin’s interceptor chain, and log the effective sender policy so an active restriction is obvious at boot. In SMTP mode, fail setup when the transport cannot be verified: a bad host or credential is a deploy-time mistake and should stop the app rather than wait for a user to approve a send that cannot work. With no SMTP credentials the runtime is in file/outbox mode (only when EMAIL_OUTBOX_MODE is set), logged loudly here so it is obvious mail is being written to disk rather than sent.

Promise<void>

Plugin.setup


shutdown(): Promise<void>

Close the SMTP connection pool. Idempotent.

Promise<void>