npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dbx-tools/email

v0.6.211

Published

Readme

@dbx-tools/email

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

Import this package when an AppKit or Mastra backend needs model-drafted outbound email with human approval, sender derivation, SMTP dispatch, and a local outbox mode for development. AppKit ships no first-party email surface, so this is additive rather than an alternative to a native plugin. Browser-safe message/result schemas live in @dbx-tools/shared-email, and the reusable React Email presentation lives in @dbx-tools/shared-email-template.

Key features:

  • 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 The AppKit Plugin

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

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

await createApp({
  plugins: [
    server(),
    lakebase(),
    emailPlugin.email({
      smtp: { host: "smtp.example.com", user: "apikey", password: process.env.SMTP_KEY },
      domain: "mail.example.com",
    }),
    mastraPlugin.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 loudly 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.

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

| Option | Type | Default | Description | | ---------------- | ------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------- | | smtp.host | string | SMTP_HOST | SMTP server hostname. Omit the whole smtp block to run in outbox mode. | | smtp.port | number | SMTP_PORT, then 587 | SMTP server port. | | smtp.secure | boolean | SMTP_SECURE, then port === 465 | TLS-on-connect socket rather than STARTTLS. | | smtp.user | string | SMTP_USER | SMTP auth username. | | smtp.password | string | SMTP_PASSWORD | SMTP auth password or API key. | | domain | string | EMAIL_DOMAIN | Domain the sender is derived on, as <user-local-part>@<domain>. | | from | string | EMAIL_FROM | Optional fixed From. Skips per-user derivation. | | systemFrom | string | EMAIL_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. | | allowedSenders | string \| string[] | EMAIL_ALLOWED_SENDERS | Permitted From patterns: exact addresses, *@domain, a bare domain, or *. | | outDir | string | EMAIL_OUTBOX_DIR, then <cwd>/tmp | Directory the outbox writes HTML previews to. | | brand | EmailBrand | dbx-tools brand | Colors, 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 variable | Purpose | | ----------------------- | -------------------------------------------------------------- | | SMTP_HOST | SMTP server hostname. | | SMTP_PORT | SMTP server port. | | SMTP_SECURE | Force or disable a TLS-on-connect socket. | | SMTP_USER | SMTP auth username. | | SMTP_PASSWORD | SMTP auth password or API key. | | EMAIL_DOMAIN | Domain for the derived sender address. | | EMAIL_FROM | Optional fixed From address, overriding per-user derivation. | | EMAIL_SYSTEM_FROM | From for system mail; defaults to no-reply@<domain>. | | EMAIL_SENDER_POLICY | allowlist (default) or unrestricted. | | EMAIL_ALLOWED_SENDERS | Comma- or whitespace-separated From allow-list. | | EMAIL_OUTBOX_MODE | Opt in to writing messages to disk when SMTP is unconfigured. | | EMAIL_OUTBOX_DIR | Directory 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 something PARSES. A fourth argument replaces just 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 looks perfect. Keep the same information in both parts - a text alternative that disagrees with the HTML reads as phishing to spam filters. 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(), emailPlugin.email()] });
await appkit.email.sendEmail(message, "[email protected]");

How Sends Reach AppKit's 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.

Nothing else has to be wired up. Two consequences are worth knowing:

  • 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 actionable 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

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.

| Tool | Runtime | Wiring | | ------------ | ------- | ------------------------------------------------------------------------ | | send_email | Mastra | tools: () => ({ send_email: emailTool.emailTool() }) | | email.send | AppKit | plugins.email.toolkit() in code, or a plugin:email frontmatter entry |

import { createApp, server } from "@databricks/appkit";
import { agents, createAgent } from "@databricks/appkit/beta";
import { plugin as emailPlugin } 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(), emailPlugin.email(), agents({ agents: { support } })],
});

AppKit Routes

| Method | Path | Response | | ------ | -------------------- | ---------------------------------------------------------- | | GET | /api/email/senders | email.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 entirely optional - set it only when every message really should come from a single mailbox.

Mail that no user asked for - a sign-in code, a password reset, 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 on purpose (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 just to spell the local part differently. The system sender is folded into the effective allow-list, 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 allow-list 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() remains available for compatibility, while markdown.markdownToHtml() now 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 { brand, plugin } from "@dbx-tools/email";

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

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

// Or hand-build the small email-safe slice:
plugin.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

| Constant | Value | Bounds | | ----------------------------- | ------- | ---------------------------------------- | | MAX_BODY_CHARS | 200,000 | Email content length. | | MAX_ATTACHMENT_BYTES | 10 MiB | One attachment's decoded inline content. | | MAX_ATTACHMENTS_TOTAL_BYTES | 20 MiB | Combined decoded attachment content. | | MAX_ATTACHMENT_COUNT | 20 | Attachments on one message. | | SEND_TIMEOUT_MS | 30,000 | One SMTP conversation. | | VERIFY_TIMEOUT_MS | 15,000 | The 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()), allow-list 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 - not as a second implementation. 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.