@dbx-tools/email 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -28,11 +28,11 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@databricks/appkit": "0.81.0",
31
- "@dbx-tools/appkit": "^0.9.5",
32
- "@dbx-tools/core": "^0.9.5",
33
- "@dbx-tools/shared-core": "^0.9.5",
34
- "@dbx-tools/shared-email": "^0.9.5",
35
- "@dbx-tools/shared-email-template": "^0.9.5",
31
+ "@dbx-tools/appkit": "^0.9.7",
32
+ "@dbx-tools/core": "^0.9.7",
33
+ "@dbx-tools/shared-core": "^0.9.7",
34
+ "@dbx-tools/shared-email": "^0.9.7",
35
+ "@dbx-tools/shared-email-template": "^0.9.7",
36
36
  "@mastra/core": "1.71.0",
37
37
  "@react-email/render": "^2.1.0",
38
38
  "nodemailer": "^7.0.13",
@@ -53,7 +53,7 @@
53
53
  "./package.json": "./package.json"
54
54
  }
55
55
  },
56
- "version": "0.9.5",
56
+ "version": "0.9.7",
57
57
  "types": "./lib/index.d.ts",
58
58
  "type": "module",
59
59
  "exports": {
@@ -66,6 +66,7 @@
66
66
  "files": [
67
67
  "index.ts",
68
68
  "src",
69
+ "exports.ts",
69
70
  "lib"
70
71
  ],
71
72
  "dbxToolsConfig": {
package/src/config.ts CHANGED
@@ -26,7 +26,7 @@
26
26
  */
27
27
  import { resolve } from "node:path";
28
28
  import { ConfigurationError, ValidationError, type BasePluginConfig } from "@databricks/appkit";
29
- import { config as coreConfig } from "@dbx-tools/core";
29
+ import { configUtils } from "@dbx-tools/core";
30
30
  import { object } from "@dbx-tools/shared-core";
31
31
  import type { JSONSchema7 } from "json-schema";
32
32
  import { defaultEmailBrand, type EmailBrand } from "./brand.ts";
@@ -272,13 +272,15 @@ export const EMAIL_CONFIG_SCHEMA: JSONSchema7 = {
272
272
  /** Parse the `SMTP_SECURE` env / config flag, defaulting by port. */
273
273
  function resolveSecure(flag: boolean | undefined, port: number): boolean {
274
274
  return (
275
- coreConfig.boolean(flag, "SMTP_SECURE", coreConfig.ENV_ONLY) ?? port === IMPLICIT_TLS_SMTP_PORT
275
+ configUtils.boolean(flag, "SMTP_SECURE", configUtils.ENV_ONLY) ??
276
+ port === IMPLICIT_TLS_SMTP_PORT
276
277
  );
277
278
  }
278
279
 
279
280
  /** Parse the `EMAIL_SENDER_POLICY` env / config value, defaulting to deny-by-default. */
280
281
  function resolveSenderPolicy(policy: SenderPolicy | undefined): SenderPolicy {
281
- const raw = policy ?? coreConfig.text("EMAIL_SENDER_POLICY", coreConfig.ENV_ONLY)?.toLowerCase();
282
+ const raw =
283
+ policy ?? configUtils.text("EMAIL_SENDER_POLICY", configUtils.ENV_ONLY)?.toLowerCase();
282
284
  if (raw === "unrestricted") return "unrestricted";
283
285
  if (raw === undefined || raw === "" || raw === "allowlist") return "allowlist";
284
286
  throw ValidationError.invalidValue("senderPolicy", raw, '"allowlist" or "unrestricted"');
@@ -311,7 +313,7 @@ function impliedSenderPatterns(sender: {
311
313
 
312
314
  /** Whether `EMAIL_OUTBOX_MODE` explicitly opts into the file/outbox fallback. */
313
315
  function isOutboxModeEnabled(): boolean {
314
- return coreConfig.boolean(undefined, "EMAIL_OUTBOX_MODE", coreConfig.ENV_ONLY) ?? false;
316
+ return configUtils.boolean(undefined, "EMAIL_OUTBOX_MODE", configUtils.ENV_ONLY) ?? false;
315
317
  }
316
318
 
317
319
  const SMTP_REQUIRED_FIELDS = ["SMTP_HOST", "SMTP_USER", "SMTP_PASSWORD"] as const;
@@ -347,15 +349,19 @@ function missingSmtpFields(
347
349
  */
348
350
  export function resolveEmailConfig(config: EmailPluginConfig = {}): ResolvedEmailConfig {
349
351
  const smtp = config.smtp ?? {};
350
- const host = coreConfig.string(smtp.host, "SMTP_HOST", coreConfig.ENV_ONLY);
351
- const user = coreConfig.string(smtp.user, "SMTP_USER", coreConfig.ENV_ONLY);
352
- const pass = coreConfig.string(smtp.password, "SMTP_PASSWORD", coreConfig.ENV_ONLY);
353
- const domain = coreConfig.string(config.domain, "EMAIL_DOMAIN", coreConfig.ENV_ONLY);
354
- const from = coreConfig.string(config.from, "EMAIL_FROM", coreConfig.ENV_ONLY);
355
- const systemFrom = coreConfig.string(config.systemFrom, "EMAIL_SYSTEM_FROM", coreConfig.ENV_ONLY);
352
+ const host = configUtils.string(smtp.host, "SMTP_HOST", configUtils.ENV_ONLY);
353
+ const user = configUtils.string(smtp.user, "SMTP_USER", configUtils.ENV_ONLY);
354
+ const pass = configUtils.string(smtp.password, "SMTP_PASSWORD", configUtils.ENV_ONLY);
355
+ const domain = configUtils.string(config.domain, "EMAIL_DOMAIN", configUtils.ENV_ONLY);
356
+ const from = configUtils.string(config.from, "EMAIL_FROM", configUtils.ENV_ONLY);
357
+ const systemFrom = configUtils.string(
358
+ config.systemFrom,
359
+ "EMAIL_SYSTEM_FROM",
360
+ configUtils.ENV_ONLY,
361
+ );
356
362
  const senderPolicy = resolveSenderPolicy(config.senderPolicy);
357
363
  const configuredSenders = parseAllowedSenders(
358
- config.allowedSenders ?? coreConfig.text("EMAIL_ALLOWED_SENDERS", coreConfig.ENV_ONLY),
364
+ config.allowedSenders ?? configUtils.text("EMAIL_ALLOWED_SENDERS", configUtils.ENV_ONLY),
359
365
  );
360
366
  const allowedSenders =
361
367
  configuredSenders.length > 0 || senderPolicy === "unrestricted"
@@ -384,7 +390,7 @@ export function resolveEmailConfig(config: EmailPluginConfig = {}): ResolvedEmai
384
390
  "Set EMAIL_DOMAIN to derive <user-local-part>@<domain> (and no-reply@<domain> for system mail), or EMAIL_FROM for a fixed address.",
385
391
  );
386
392
  }
387
- const port = coreConfig.port(smtp.port, "SMTP_PORT", DEFAULT_SMTP_PORT, coreConfig.ENV_ONLY);
393
+ const port = configUtils.port(smtp.port, "SMTP_PORT", DEFAULT_SMTP_PORT, configUtils.ENV_ONLY);
388
394
  return {
389
395
  mode: "smtp",
390
396
  host: host!,
@@ -403,7 +409,7 @@ export function resolveEmailConfig(config: EmailPluginConfig = {}): ResolvedEmai
403
409
  }
404
410
 
405
411
  const outDir = resolve(
406
- coreConfig.string(config.outDir, "EMAIL_OUTBOX_DIR", coreConfig.ENV_ONLY) ??
412
+ configUtils.string(config.outDir, "EMAIL_OUTBOX_DIR", configUtils.ENV_ONLY) ??
407
413
  resolve(process.cwd(), "tmp"),
408
414
  );
409
415
  return { mode: "file", outDir, ...sender };
package/src/email-html.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /** Node rendering adapters for the shared React Email document. @module */
2
- import { string } from "@dbx-tools/shared-core";
2
+ import { stringUtils } from "@dbx-tools/shared-core";
3
3
  import { EmailDocument, type EmailDocumentProps } from "@dbx-tools/shared-email-template";
4
4
  import { render } from "@react-email/render";
5
5
  import { createElement } from "react";
6
6
 
7
7
  /** Escape HTML-significant characters (re-exported from shared-core). */
8
- export const escapeHtml = string.escapeHtml;
8
+ export const escapeHtml = stringUtils.escapeHtml;
9
9
 
10
10
  /** Options accepted by the shared React Email document. */
11
11
  export type EmailHtmlOptions = EmailDocumentProps;
package/src/outbox.ts CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  import { mkdir, writeFile } from "node:fs/promises";
17
17
  import { join, resolve } from "node:path";
18
- import { string } from "@dbx-tools/shared-core";
18
+ import { stringUtils } from "@dbx-tools/shared-core";
19
19
  import type { EmailMessage } from "@dbx-tools/shared-email";
20
20
  import type { EmailBrand } from "./brand.ts";
21
21
  import { renderEmailHtml, type EmailHtmlOptions } from "./email-html.ts";
@@ -28,7 +28,7 @@ const SUBJECT_SLUG_MAX_LENGTH = 48;
28
28
 
29
29
  /** Filesystem-safe slug of the subject for the file name. */
30
30
  function subjectSlug(subject: string): string {
31
- return string.toSlugWithOptions({ maxLength: SUBJECT_SLUG_MAX_LENGTH }, subject) || "email";
31
+ return stringUtils.toSlugWithOptions({ maxLength: SUBJECT_SLUG_MAX_LENGTH }, subject) || "email";
32
32
  }
33
33
 
34
34
  /** The envelope rows shown above the body in the preview file. */
package/src/plugin.ts CHANGED
@@ -29,7 +29,6 @@ import {
29
29
  ConnectionError,
30
30
  ExecutionError,
31
31
  getExecutionContext,
32
- Plugin,
33
32
  toPlugin,
34
33
  ValidationError,
35
34
  type AppKitError,
@@ -37,16 +36,9 @@ import {
37
36
  type IAppRouter,
38
37
  type PluginManifest,
39
38
  } from "@databricks/appkit";
40
- import {
41
- defineTool,
42
- executeFromRegistry,
43
- toolsFromRegistry,
44
- type AgentToolDefinition,
45
- type ToolProvider,
46
- type ToolRegistry,
47
- } from "@databricks/appkit/beta";
48
- import { brand as appkitBrand } from "@dbx-tools/appkit";
49
- import { log, string, token } from "@dbx-tools/shared-core";
39
+ import { defineTool, type ToolRegistry } from "@databricks/appkit/beta";
40
+ import { brandContext, ToolRegistryPlugin } from "@dbx-tools/appkit";
41
+ import { log, stringUtils, token } from "@dbx-tools/shared-core";
50
42
  import {
51
43
  email as sharedEmail,
52
44
  type EmailMessage,
@@ -82,12 +74,12 @@ const logger = log.logger("email");
82
74
  * @example
83
75
  * ```ts
84
76
  * import { createApp, server } from "@databricks/appkit";
85
- * import { plugin as emailPlugin } from "@dbx-tools/email";
77
+ * import { email } from "@dbx-tools/email";
86
78
  *
87
79
  * await createApp({
88
80
  * plugins: [
89
81
  * server(),
90
- * emailPlugin.email({
82
+ * email({
91
83
  * smtp: { host: "smtp.example.com", user: "apikey", password: process.env.SMTP_KEY },
92
84
  * domain: "mail.example.com",
93
85
  * }),
@@ -95,7 +87,7 @@ const logger = log.logger("email");
95
87
  * });
96
88
  * ```
97
89
  */
98
- export class EmailPlugin extends Plugin<EmailPluginConfig> implements ToolProvider {
90
+ export class EmailPlugin extends ToolRegistryPlugin<EmailPluginConfig> {
99
91
  static manifest = {
100
92
  name: "email",
101
93
  displayName: "Email",
@@ -121,7 +113,7 @@ export class EmailPlugin extends Plugin<EmailPluginConfig> implements ToolProvid
121
113
  * against the same schema first, but re-parsing is what gives the body typed
122
114
  * arguments instead of `unknown`.
123
115
  */
124
- private readonly tools: ToolRegistry = {
116
+ protected readonly toolRegistry: ToolRegistry = {
125
117
  [SEND_TOOL]: defineTool({
126
118
  description: SEND_EMAIL_DESCRIPTION,
127
119
  schema: sharedEmail.emailMessageSchema,
@@ -146,7 +138,7 @@ export class EmailPlugin extends Plugin<EmailPluginConfig> implements ToolProvid
146
138
  override async setup(): Promise<void> {
147
139
  const { transporter, config } = getEmailRuntime({
148
140
  ...this.config,
149
- brand: this.config.brand ?? emailBrandFromContext(appkitBrand.getBrandContext()),
141
+ brand: this.config.brand ?? emailBrandFromContext(brandContext.getBrandContext()),
150
142
  });
151
143
  setEmailExecutor((fn, settings) => this.execute(fn, settings));
152
144
  const policy = {
@@ -230,7 +222,7 @@ export class EmailPlugin extends Plugin<EmailPluginConfig> implements ToolProvid
230
222
  method: "get",
231
223
  path: SENDERS_ROUTE,
232
224
  handler: async (req, res) => {
233
- const oboToken = string.trimToNull(req.header(token.ACCESS_TOKEN_HEADER));
225
+ const oboToken = stringUtils.trimToNull(req.header(token.ACCESS_TOKEN_HEADER));
234
226
  const scoped = oboToken === null ? this : this.asUser(req);
235
227
  const result = await scoped.executeListSenders();
236
228
  if (!result.ok) {
@@ -263,20 +255,6 @@ export class EmailPlugin extends Plugin<EmailPluginConfig> implements ToolProvid
263
255
  };
264
256
  }
265
257
 
266
- /** AppKit `ToolProvider`: the tool definitions offered to an agent. */
267
- getAgentTools(): AgentToolDefinition[] {
268
- return toolsFromRegistry(this.tools);
269
- }
270
-
271
- /**
272
- * AppKit `ToolProvider`: run one tool call. Arguments are validated against
273
- * the tool's schema first, and a validation failure comes back as an
274
- * LLM-friendly string so the model can correct itself on the next turn.
275
- */
276
- async executeAgentTool(name: string, args: unknown, signal?: AbortSignal): Promise<unknown> {
277
- return executeFromRegistry(this.tools, name, args, signal);
278
- }
279
-
280
258
  /**
281
259
  * Send one message, resolving the sender for the caller in scope when
282
260
  * `from` is not pinned. The interceptor chain is applied inside
@@ -362,15 +340,15 @@ function unwrap<T>(result: ExecutionResult<T>): T {
362
340
  * @example
363
341
  * ```ts
364
342
  * import { createApp, server } from "@databricks/appkit";
365
- * import { brand, plugin as emailPlugin } from "@dbx-tools/email";
343
+ * import { defaultEmailBrand, email } from "@dbx-tools/email";
366
344
  *
367
345
  * await createApp({
368
346
  * plugins: [
369
347
  * server(),
370
- * emailPlugin.email({
348
+ * email({
371
349
  * domain: "mail.example.com",
372
350
  * allowedSenders: ["*@mail.example.com"],
373
- * brand: brand.defaultEmailBrand,
351
+ * brand: defaultEmailBrand,
374
352
  * }),
375
353
  * ],
376
354
  * });
package/src/sender.ts CHANGED
@@ -28,7 +28,7 @@
28
28
  */
29
29
 
30
30
  import { ConfigurationError, ValidationError } from "@databricks/appkit";
31
- import { log, net, string } from "@dbx-tools/shared-core";
31
+ import { log, net, stringUtils } from "@dbx-tools/shared-core";
32
32
 
33
33
  import type { ResolvedEmailConfig, ResolvedSender } from "./config.ts";
34
34
 
@@ -132,11 +132,11 @@ type SenderSource = Pick<ResolvedSender, "systemFrom" | "domain" | "from">;
132
132
  * result into the sender allow-list before any send happens.
133
133
  */
134
134
  export function systemSenderAddress(source: SenderSource): string | undefined {
135
- const explicit = string.trimToNull(source.systemFrom);
135
+ const explicit = stringUtils.trimToNull(source.systemFrom);
136
136
  if (explicit) return explicit;
137
- const domain = string.trimToNull(source.domain);
137
+ const domain = stringUtils.trimToNull(source.domain);
138
138
  if (domain) return `${SYSTEM_SENDER_LOCAL_PART}@${domain}`;
139
- return string.trimToNull(source.from) ?? undefined;
139
+ return stringUtils.trimToNull(source.from) ?? undefined;
140
140
  }
141
141
 
142
142
  /**
@@ -166,7 +166,7 @@ export function resolveSenderAddress(
166
166
  config: ResolvedEmailConfig,
167
167
  userEmail: string | undefined,
168
168
  ): string {
169
- const email = string.trimToNull(userEmail);
169
+ const email = stringUtils.trimToNull(userEmail);
170
170
  if (!email) return resolveSystemSenderAddress(config);
171
171
  if (config.from) return config.from;
172
172
  if (config.domain) return deriveSenderAddress(email, config.domain);
package/src/tool.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import { getExecutionContext } from "@databricks/appkit";
21
- import { log, string } from "@dbx-tools/shared-core";
21
+ import { log, stringUtils } from "@dbx-tools/shared-core";
22
22
  import { email } from "@dbx-tools/shared-email";
23
23
  import { createTool, type ToolAction } from "@mastra/core/tools";
24
24
  import { resolveSenderAddress } from "./sender.ts";
@@ -31,7 +31,7 @@ const logger = log.logger("email/tool/send-email");
31
31
  * {@link emailTool} and the AppKit `email.send` tool so both agents get the
32
32
  * same guidance about approval and scope.
33
33
  */
34
- export const SEND_EMAIL_DESCRIPTION = string.toDescription(`
34
+ export const SEND_EMAIL_DESCRIPTION = stringUtils.toDescription(`
35
35
  Send an email on the user's behalf. Pass one or more recipient
36
36
  addresses (with optional cc / bcc and file attachments), a subject,
37
37
  and a body; the configured approval policy runs before it goes out.
package/src/transport.ts CHANGED
@@ -22,12 +22,8 @@
22
22
  * @module
23
23
  */
24
24
 
25
- import {
26
- ConfigurationError,
27
- ExecutionError,
28
- ValidationError,
29
- type ExecutionResult,
30
- } from "@databricks/appkit";
25
+ import { ConfigurationError, ValidationError, type ExecutionResult } from "@databricks/appkit";
26
+ import { pluginExecution } from "@dbx-tools/appkit";
31
27
  import { execution, log } from "@dbx-tools/shared-core";
32
28
  import type { EmailAttachment, EmailMessage, EmailResult } from "@dbx-tools/shared-email";
33
29
  import nodemailer, { type SendMailOptions, type Transporter } from "nodemailer";
@@ -146,23 +142,14 @@ export async function executeWrite<T>(
146
142
  signal?: AbortSignal,
147
143
  ): Promise<T> {
148
144
  const { execute } = getEmailRuntime();
149
- return execution.run({
145
+ return pluginExecution.runPluginExecution({
146
+ plugin: "email",
147
+ logger,
150
148
  operation,
151
149
  settings,
152
150
  execute,
153
151
  fn,
154
152
  signal,
155
- canceled: ExecutionError.canceled,
156
- failed: (failure) => {
157
- logger.warn("execution-failed", {
158
- operation: failure.operation,
159
- status: failure.status,
160
- error: failure.message,
161
- });
162
- return new ExecutionError(`email: ${failure.operation} failed`, {
163
- context: { operation: failure.operation, status: failure.status },
164
- });
165
- },
166
153
  });
167
154
  }
168
155