@dbx-tools/email

中文文档
0.9.206发布于 2026/11/11Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 8277

Server-side email runtime, agent tools, and AppKit plugin

原始使用文档
文档目录17 节
  1. Quick start
  2. Register delivery
  3. Configuration
  4. Send without an agent
  5. Control the visible heading
  6. Control the plain-text part
  7. Control the preheader (and the push notification)
  8. How sends reach AppKit interceptors
  9. Resolve SMTP or outbox mode
  10. Agent tools and approval
  11. AppKit routes
  12. Sender addresses
  13. Render a React Email
  14. Brand the email
  15. Use the outbox in tests
  16. Limits
  17. Modules

@dbx-tools/email

Add model-drafted email with human approval, sender policy, SMTP delivery, and a local development outbox to an AppKit or Mastra backend. Browser-safe message and result schemas live in @dbx-tools/shared-email, and the reusable React Email presentation lives in @dbx-tools/shared-email-template.

Quick start

Give an agent an email tool that pauses before delivery:

import { emailTool } from "@dbx-tools/email";
import { agents } from "@dbx-tools/appkit-mastra";

const support = agents.createAgent({
  instructions: "Draft the reply and request approval before sending it.",
  tools: () => ({ send_email: emailTool() }),
});

The package:

  • AppKit plugin registration that resolves config, verifies SMTP at boot, and mounts a sender-options route.
  • Two agent surfaces over one runtime: a Mastra send_email tool that suspends for human approval, and an AppKit email.send tool annotated as a write so a host's approval gate fires.
  • SMTP delivery for production and HTML outbox delivery for local development and tests.
  • Sender derivation from the current Databricks user: EMAIL_DOMAIN alone is enough, with a fixed EMAIL_FROM as an override rather than a requirement.
  • A separate do-not-reply sender for system mail (no-reply@<domain>, or EMAIL_SYSTEM_FROM), used automatically when no user is in scope.
  • Deny-by-default sender policy with exact addresses, domains, domain wildcards, and a named unrestricted escape hatch.
  • React Email rendering with responsive components, matching HTML/plain-text alternatives, metadata, attachment summaries, and the dbx-tools brand by default.
  • Named caps on body length and attachment size, and an AbortSignal threaded through every send.

Register delivery

import { createApp, lakebase, server } from "@databricks/appkit";
import { email, emailTool } from "@dbx-tools/email";
import { agents, mastra } from "@dbx-tools/appkit-mastra";

const support = agents.createAgent({
  instructions: "Draft emails, but wait for approval before sending.",
  tools: () => ({ send_email: emailTool() }),
});

await createApp({
  plugins: [
    server(),
    lakebase(),
    email({
      smtp: { host: "smtp.example.com", user: "apikey", password: process.env.SMTP_KEY },
      domain: "mail.example.com",
    }),
    mastra({ agents: support, storage: true }),
  ],
});

plugin.email() validates config, primes the shared runtime, verifies SMTP when SMTP mode is active, and mounts a sender-options route for UIs. A failed verify fails setup, so a bad host or credential shows up in the boot logs rather than on the first approved send. Outbox mode skips the check and logs a warning instead.

tool.emailTool() creates an approval-gated Mastra send_email tool. Approval requires Mastra storage, so register lakebase() or configure storage explicitly in the Mastra plugin.

emailTool({ requireApproval }) also accepts Mastra's native conditional approval function. Use it with a Mastra Classifier or a durable, user-scoped fingerprint policy when previously reviewed calls should reduce future prompts:

const sendEmail = emailTool({
  requireApproval: async (input, { requestContext } = {}) =>
    approvalPolicy.requiresReview({
      input,
      userId: requestContext?.["mastra__resourceId"],
    }),
});

The callback returns true when human review is still required and false when Mastra may run the tool. Fail closed in the policy. Regular Mastra agents do not provide a built-in durable "remember this approval" store. Agent Controller permission rules are a separate runtime, so this package does not emulate them client-side.

The plugin does not decide how approval is presented. It emits a Mastra tool suspension and expects the host UI to resume that tool call with an approval or denial result. @dbx-tools/ui/email provides the matching approval card and compose components.

Configuration

OptionTypeDefaultDescription
smtp.hoststringSMTP_HOSTSMTP server hostname. Omit the whole smtp block to run in outbox mode.
smtp.portnumberSMTP_PORT, then 587SMTP server port.
smtp.securebooleanSMTP_SECURE, then port === 465TLS-on-connect socket rather than STARTTLS.
smtp.userstringSMTP_USERSMTP auth username.
smtp.passwordstringSMTP_PASSWORDSMTP auth password or API key.
domainstringEMAIL_DOMAINDomain the sender is derived on, as <user-local-part>@<domain>.
fromstringEMAIL_FROMOptional fixed From. Skips per-user derivation.
systemFromstringEMAIL_SYSTEM_FROM, then no-reply@<domain>From for system mail (no user in scope).
senderPolicy"allowlist" | "unrestricted"EMAIL_SENDER_POLICY, then "allowlist"How the sender is restricted when allowedSenders is empty.
allowedSendersstring | string[]EMAIL_ALLOWED_SENDERSPermitted From patterns: exact addresses, *@domain, a bare domain, or *.
outDirstringEMAIL_OUTBOX_DIR, then <cwd>/tmpDirectory the outbox writes HTML previews to.
brandEmailBranddbx-tools brandColors, font, display name, footer, and optional logo applied to every message.

Precedence per field is explicit config, then the environment variable, then the built-in default.

Environment variablePurpose
SMTP_HOSTSMTP server hostname.
SMTP_PORTSMTP server port.
SMTP_SECUREForce or disable a TLS-on-connect socket.
SMTP_USERSMTP auth username.
SMTP_PASSWORDSMTP auth password or API key.
EMAIL_DOMAINDomain for the derived sender address.
EMAIL_FROMOptional fixed From address, overriding per-user derivation.
EMAIL_SYSTEM_FROMFrom for system mail. Defaults to no-reply@<domain>.
EMAIL_SENDER_POLICYallowlist (default) or unrestricted.
EMAIL_ALLOWED_SENDERSComma- or whitespace-separated From allowlist.
EMAIL_OUTBOX_MODEOpt in to writing messages to disk when SMTP is unconfigured.
EMAIL_OUTBOX_DIRDirectory for outbox previews.

The SMTP_* names are unprefixed because SMTP is a third-party service, not a Databricks resource.

Send without an agent

import { transport } from "@dbx-tools/email";

const result = await transport.sendEmail(
  {
    to: ["[email protected]"],
    cc: ["[email protected]"],
    subject: "Daily report",
    body: "# Report\nEverything completed.",
    attachments: [{ filename: "report.csv", content: "a,b\n1,2\n" }],
  },
  "[email protected]",
);

Use direct sends for operational mail, tests, or admin flows where a model is not involved. The same resolved runtime is used by the AppKit plugin and both agent tools, so every path shares one connection pool, one sender policy, and one set of caps. A third argument accepts an AbortSignal when the caller wants to stop waiting on SMTP.

Control the visible heading

The HTML card normally repeats the transport subject as its heading. Set heading when the subject contains notification-specific context that should not be repeated inside the opened message:

await transport.sendEmail(message, from, undefined, {
  heading: "Verification code",
});

This keeps a code-bearing subject useful in inboxes and notifications while the HTML body presents the code once.

Control the plain-text part

By default both MIME alternatives are rendered from one React Email tree, so the text/plain part is a rendering of the HTML. It carries the brand header and footer, and CSS margin becomes blank lines. That is right for prose and wrong for content that a client parses. A fourth argument replaces only that part, leaving the branded HTML untouched:

await transport.sendEmail(message, from, undefined, {
  text: `Your verification code is: ${code}\nThis code expires in 10 minutes.`,
});

The motivating case is a one-time code: client code detection reads the code out of the prompt line, and a styled <h2> code renders two blank lines below the prompt, so autofill stops being offered even though the HTML renders correctly. Keep the same information in both parts. Spam filters can treat a text alternative that disagrees with the HTML as phishing. See @dbx-tools/tunnel for a gate that does this.

Control the preheader (and the push notification)

The same options object takes a preview, the preheader: the snippet a client shows beside the subject in an inbox list, and the body of the push notification a mobile mail app posts. It defaults to the subject.

await transport.sendEmail(message, from, undefined, {
  preview: `Your verification code is: ${code}`,
});

It matters for anything a phone reads off a notification rather than out of the open message. Mobile one-time-code autofill is the case in point: the notification carries the sender, the subject, and this snippet, so a code that appears only in the body is never offered. Put the important text in the subject and the preheader and the body becomes what a human reads, not what a heuristic depends on.

The plugin export is equivalent and resolves the sender for you when the caller is a Databricks user:

const appkit = await createApp({ plugins: [server(), email()] });
await appkit.email.sendEmail(message, "[email protected]");

How sends reach AppKit interceptors

Every send runs through AppKit's interceptor chain (timeout, telemetry, and the retry / cache posture in defaults), including sends from the Mastra tool, which has no plugin instance in scope. The runtime carries an executor slot for this: registering the plugin installs its own Plugin.execute() there at setup, and sendEmail() routes through whatever is installed.

This has two effects:

  • In a plain Mastra app with no AppKit plugin registered, the slot falls back to running the send directly, so the tool still works without interceptors.
  • Recipient, cap, and sender-policy checks run before the chain, so a rejected message keeps its specific status and clear message. A failure inside the chain is re-raised as a stable ExecutionError, so an SMTP relay's own wording never becomes the caller's or the model's error text.

Resolve SMTP or outbox mode

import { config, transport } from "@dbx-tools/email";

const resolved = config.resolveEmailConfig({
  smtp: { host: "smtp.example.com", user: "apikey", password: secret },
  domain: "mail.example.com",
});

const runtime = transport.getEmailRuntime({
  smtp: { host: "smtp.example.com", user: "apikey", password: secret },
  domain: "mail.example.com",
});

resolveEmailConfig() returns the validated ResolvedEmailConfig for inspection. getEmailRuntime() takes the same plugin config, resolves it, and memoizes the transport process-wide. The plugin primes it at setup, so later callers pass nothing and get the same instance.

SMTP mode requires host, user, password, and a sender source. Outbox mode writes HTML files to disk when SMTP credentials are absent and EMAIL_OUTBOX_MODE=1.

Use SMTP mode for deployed apps. Use outbox mode for local demos, automated tests, and development loops where sending real mail would be risky.

Agent tools and approval

Two tools expose the same send capability, one per agent runtime. Both are gated: the Mastra tool suspends for approval, and the AppKit tool is annotated { effect: "write", requiresUserContext: true } so a host's approval gate fires. The AppKit tool is deliberately not autoInheritable, so an agent reaches it only by wiring it explicitly.

Approval applies to the agent tools. transport.sendEmail() and the AppKit plugin's sendEmail() export send immediately, so direct callers must enforce their own authorization and approval policy.

ToolRuntimeWiring
send_emailMastratools: () => ({ send_email: emailTool() })
email.sendAppKitplugins.email.toolkit() in code, or a plugin:email frontmatter entry
import { createApp, server } from "@databricks/appkit";
import { agents, createAgent } from "@databricks/appkit/beta";
import { email } from "@dbx-tools/email";

const support = createAgent({
  instructions: "Draft emails, but wait for approval before sending.",
  tools: (plugins) => ({ ...plugins.email.toolkit() }),
});

await createApp({
  plugins: [server(), email(), agents({ agents: { support } })],
});

AppKit routes

MethodPathResponse
GET/api/email/sendersemail.emailSendersSchema from @dbx-tools/shared-email.

The response includes the concrete sender addresses the current user may choose, the default sender address, and whether the list was restricted by policy. The route runs in the on-behalf-of user scope so domain wildcards resolve against the caller's own local part.

Use this route to populate a From dropdown in a compose UI. If no dropdown is shown, the server can still derive the sender from the active user and config.

Sender addresses

import { config, sender } from "@dbx-tools/email";

const resolved = config.resolveEmailConfig({ domain: "mail.example.com" });

// A user caused this send, so it comes from them: [email protected]
const from = sender.resolveSenderAddress(resolved, "[email protected]");

// Nobody did, so it is system mail: [email protected]
const system = sender.resolveSystemSenderAddress(resolved);

sender.assertSenderAllowed(from, resolved.allowedSenders);

domain is the only sender setting a deployment needs. The From for a normal send is the on-behalf-of user's local part re-homed on it, so a recipient can reply to the person who caused the message. from overrides that with one fixed address and is optional. Set it only when every message really should come from a single mailbox.

Mail that no user asked for, such as a sign-in code, a password reset, or an alert, is system mail, and a reply to it reaches nobody. It sends from no-reply@<domain> instead, which is also what resolveSenderAddress() returns when there is no user in scope, so a service-context send never leaks a person's address onto machine mail. That default outranks from because a fixed from is normally a human or team address. Name the address yourself with systemFrom / EMAIL_SYSTEM_FROM to route those replies to a monitored support@, or to use a different local part. The system sender is folded into the effective allowlist, so the deny-by-default policy never blocks the app's own sign-in mail.

Sender helpers support exact addresses, domain wildcards, bare domains, and *. sender.listSenderOptions(resolved, userEmail) produces the concrete From choices for the current user, which is what the AppKit plugin exposes to clients.

The default senderPolicy: "allowlist" is deny-by-default: with no explicit allowedSenders, the effective allowlist is the configured sender source, so a deployment that only sets EMAIL_DOMAIN rejects a From on any other domain. Set senderPolicy: "unrestricted" to accept any From a caller supplies. The effective policy is logged at boot.

Render a React Email

import { emailHtml, markdown } from "@dbx-tools/email";

const html = await emailHtml.renderEmailHtml({
  subject: "Incident update",
  body: "## Status\nResolved.",
});

const text = await emailHtml.renderEmailText({
  subject: "Incident update",
  body: "## Status\nResolved.",
});

const fragment = await markdown.markdownToHtml("## Status\nResolved.");

renderEmailHtml() and renderEmailText() render the same shared React Email component tree into the two MIME alternatives used by SMTP. The universal components live in @dbx-tools/shared-email-template, so the browser preview and delivered message share typography, content styling, and brand behavior. markdown.normalizeMarkdown() normalizes Markdown without rendering it. markdown.markdownToHtml() renders through the shared React Email body.

Brand the email

Every message uses the repository's dbx-tools brand by default. Pass a brand to the plugin or renderer only when the consuming application needs its own identity.

import { defaultEmailBrand, email, emailBrandFromContext } from "@dbx-tools/email";

// Explicitly select the same brand used by default:
email({ brand: defaultEmailBrand });

// Or derive from any shared BrandContext:
import { brandUtils } from "@dbx-tools/shared-core";
email({ brand: emailBrandFromContext(brandUtils.defaultBrandContext) });

// Or hand-build the small email-safe slice:
email({
  brand: {
    accent: "#FF3621",
    fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif",
    name: "Acme",
    logoUrl: "https://acme.example/logo.svg", // http(s): or data: only
  },
});

React Email applies the brand through email-safe inline styles. The browser UI's [data-brand] CSS bridge cannot reach an inbox, so both the delivered document and browser preview consume the same EmailBrand values directly. A logoUrl renders when it is an http(s):, data:, or cid: URL. Otherwise the branded display name becomes the header mark.

Use the outbox in tests

import { outbox } from "@dbx-tools/email";

const path = await outbox.writeOutboxEmail(message, "[email protected]", "tmp/email-outbox");

Outbox files are HTML previews with metadata in the header, written to <dir>/<from>/<timestamp>-<subject-slug>.html. A fourth argument accepts an EmailBrand. A fifth accepts the same heading and preview presentation used by the transport. Attachments are listed in the preview, but attachment bytes are not copied to disk.

Limits

ConstantValueBounds
MAX_BODY_CHARS200,000Email content length.
MAX_ATTACHMENT_BYTES10 MiBOne attachment's decoded inline content.
MAX_ATTACHMENTS_TOTAL_BYTES20 MiBCombined decoded attachment content.
MAX_ATTACHMENT_COUNT20Attachments on one message.
SEND_TIMEOUT_MS30,000One SMTP conversation.
VERIFY_TIMEOUT_MS15,000The setup-time SMTP handshake.

An oversized payload is rejected with a ValidationError before anything is handed to SMTP. The constants and the plugin's interceptor settings live in the defaults module. Attachments accept inline content only. Local paths and remote URLs are not part of the model-facing contract.

Modules

  • plugin - EmailPlugin, the email() AppKit plugin factory, the sender route, and the email.send AppKit agent tool.
  • tool - approval-gated emailTool() Mastra tool and the shared SEND_EMAIL_DESCRIPTION.
  • transport - shared runtime, getEmailRuntime(), resetEmailRuntime(), verifyEmailTransport(), sendEmail(), its SendEmailOptions (an explicit HTML heading, text/plain alternative, and preview preheader), and the executor slot (setEmailExecutor(), executeWrite()) that puts every send on AppKit's interceptor chain.
  • config - SMTP/outbox config types, sender policy, JSON schema, and resolveEmailConfig().
  • defaults - execution settings for the interceptor chain and the payload caps.
  • sender - per-user sender derivation, the do-not-reply system sender (resolveSystemSenderAddress()), allowlist parsing, and sender-option listing.
  • emailHtml - Node rendering adapters over the shared template: renderEmailHtml(), renderEmailText(), and renderEmail() for both MIME alternatives from one component tree.
  • markdown - body-only rendering plus normalizeMarkdown().
  • outbox - local HTML file writer for development and tests.
  • brand - EmailBrand, emailBrandFromContext(), and defaultEmailBrand.

brand, and the components behind emailHtml/markdown, re-export from @dbx-tools/shared-email-template. They exist so server code has one import for delivery, and so the Node renderer and a browser preview cannot drift apart. Style inlining is React Email's own. This package runs no separate CSS inliner.

Pair this package with @dbx-tools/shared-email when a UI or tool schema needs to validate the same email payload, and with @dbx-tools/shared-email-template when another runtime needs to reuse the presentation components directly.

版本与发布记录

@dbx-tools/email 共发布 357 个版本,首次发布于 2026/07/18;过去 12 个月发布了 357 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.9.206latest2026/11/11今天
0.9.2052026/11/111 天前
0.9.2032026/11/074 天前
0.9.2022026/11/065 天前
0.9.2012026/11/056 天前
0.9.2002026/11/056 天前

关于 @dbx-tools/email 的常见问题

@dbx-tools/email 是什么?

Server-side email runtime, agent tools, and AppKit plugin

如何安装 @dbx-tools/email?

在项目目录运行 npm install @dbx-tools/email,或使用 pnpm add / yarn add / bun add。

@dbx-tools/email 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@dbx-tools/email 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@dbx-tools/email 可以商用吗?

@dbx-tools/email 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@dbx-tools/email 的下载量有多少?

根据 npm 官方下载统计,@dbx-tools/email 在 2026/10/12 – 2026/11/10 这 30 天内被下载 8,277 次,日均约 276 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@dbx-tools/email 还在维护吗?

最新版本 0.9.206 发布于 2026/11/11(今天);至今共发布 357 个版本,其中过去 12 个月发布了 357 个;首次发布于 2026/07/18。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。