@dbx-tools/email 0.3.44 → 0.4.0

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.
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Sender-address policy: turn the on-behalf-of user's email into an
3
+ * outbound `From`, and (optionally) restrict which addresses may send.
4
+ *
5
+ * The default `From` re-homes the local part (everything before `@`) of
6
+ * the OBO email on the configured sending domain, so `alice@databricks.com`
7
+ * through a domain of `mail.example.com` goes out as
8
+ * `alice@mail.example.com`. An explicit `from` short-circuits that; the
9
+ * file/outbox fallback (no domain) keeps the user's address verbatim so
10
+ * test artifacts land under a recognizable folder.
11
+ *
12
+ * The resolved `From` is then constrained to the effective allow-list: a
13
+ * pattern is either an exact address (`user@domain.com`), a domain wildcard
14
+ * (`*@domain.com` or the bare `domain.com`, matching any local part on that
15
+ * domain), or `*` (any). This module only matches patterns; which patterns
16
+ * apply is decided by the configured sender policy in `./config`, which
17
+ * under the default `"allowlist"` mode fills an empty list in from the
18
+ * sender source. {@link listSenderOptions} expands the effective list into
19
+ * the concrete addresses a UI dropdown can offer for the current user.
20
+ *
21
+ * @module
22
+ */
23
+ import { ConfigurationError, ValidationError } from "@databricks/appkit";
24
+ import { log, net } from "@dbx-tools/shared-core";
25
+ const logger = log.logger("email/sender");
26
+ /**
27
+ * Re-home the OBO user's local part on `domain`. Throws when no usable
28
+ * local part is available (e.g. a service-context call with no user).
29
+ */
30
+ export function deriveSenderAddress(userEmail, domain) {
31
+ const local = userEmail?.split("@")[0]?.trim();
32
+ if (!local) {
33
+ throw ConfigurationError.resourceNotFound("On-behalf-of user email", "Set `from` / EMAIL_FROM to send from a fixed address instead of deriving one.");
34
+ }
35
+ return `${local}@${domain}`;
36
+ }
37
+ /**
38
+ * Normalize a sender allow-list from config (a `string[]`) or an env var
39
+ * (a CSV / whitespace-separated string). Delegates to the shared
40
+ * {@link net.parseEmails} so allow-list patterns are read exactly
41
+ * like recipient lists elsewhere: entries are trimmed, lower-cased (so
42
+ * matching in {@link isSenderAllowed} is case-insensitive), and
43
+ * de-duplicated; empties are dropped. An empty result means "no
44
+ * restriction".
45
+ */
46
+ export function parseAllowedSenders(raw) {
47
+ return net.parseEmails(raw, { lowercase: true });
48
+ }
49
+ /** The `@domain` suffix a wildcard / bare-domain pattern matches, else null. */
50
+ function patternDomainSuffix(pattern) {
51
+ if (pattern.startsWith("*@"))
52
+ return `@${pattern.slice(2)}`;
53
+ if (!pattern.includes("@"))
54
+ return `@${pattern}`;
55
+ return null;
56
+ }
57
+ /** Whether `address` (already lower-cased) satisfies a single pattern. */
58
+ function matchesPattern(address, pattern) {
59
+ if (pattern === "*")
60
+ return true;
61
+ const suffix = patternDomainSuffix(pattern);
62
+ if (suffix)
63
+ return address.length > suffix.length && address.endsWith(suffix);
64
+ return address === pattern;
65
+ }
66
+ /**
67
+ * Whether `from` is permitted by the allow-list. An empty (or absent)
68
+ * allow-list permits everything: {@link resolveEmailConfig} is what turns
69
+ * the configured {@link SenderPolicy} into concrete patterns, so an empty
70
+ * list here means the policy had nothing to narrow to.
71
+ */
72
+ export function isSenderAllowed(from, patterns) {
73
+ if (patterns.length === 0)
74
+ return true;
75
+ const address = from.trim().toLowerCase();
76
+ return patterns.some((pattern) => matchesPattern(address, pattern));
77
+ }
78
+ /**
79
+ * Throw when `from` is not permitted by the allow-list. No-op when the
80
+ * allow-list is empty. The single enforcement point for the restriction
81
+ * (called from {@link sendEmail}), so every send path is covered whether
82
+ * the address was derived server-side or chosen in a UI.
83
+ */
84
+ export function assertSenderAllowed(from, patterns) {
85
+ if (isSenderAllowed(from, patterns))
86
+ return;
87
+ // The thrown message names the field only; the patterns are policy detail
88
+ // that belongs in the operator's logs, not in a client or model response.
89
+ logger.warn("sender:denied", { from, allowedSenders: patterns });
90
+ throw ValidationError.invalidValue("from", from, "an address permitted by the configured sender allow-list");
91
+ }
92
+ /**
93
+ * Resolve the `From` address for a send from the resolved config and the
94
+ * current OBO user: explicit `from` wins, then `<local>@<domain>`, then
95
+ * (file/outbox mode only) the user's email verbatim. Throws when none of
96
+ * those yield an address.
97
+ */
98
+ export function resolveSenderAddress(config, userEmail) {
99
+ if (config.from)
100
+ return config.from;
101
+ if (config.domain)
102
+ return deriveSenderAddress(userEmail, config.domain);
103
+ const email = userEmail?.trim();
104
+ if (!email) {
105
+ throw ConfigurationError.resourceNotFound("Email sender address", "Set `from` / EMAIL_FROM, set `domain` / EMAIL_DOMAIN, or run on behalf of a user.");
106
+ }
107
+ return email;
108
+ }
109
+ /**
110
+ * Expand the resolved config's allow-list into the concrete `From`
111
+ * addresses offered to the current user - the data a UI sender dropdown
112
+ * renders. Exact-address patterns pass through; domain wildcards
113
+ * (`*@domain.com` / bare `domain.com`) are concretized as
114
+ * `<user-local>@<domain>` and dropped when no OBO user local part is
115
+ * available. When no allow-list is configured, the single default sender
116
+ * ({@link resolveSenderAddress}) is returned when it can be resolved,
117
+ * else an empty list. The default resolved sender, when permitted, is
118
+ * ordered first.
119
+ */
120
+ export function listSenderOptions(config, userEmail) {
121
+ const patterns = config.allowedSenders ?? [];
122
+ const local = userEmail?.split("@")[0]?.trim().toLowerCase();
123
+ const options = [];
124
+ const add = (address) => {
125
+ if (address && !options.includes(address))
126
+ options.push(address);
127
+ };
128
+ // Surface the address a send would use by default first, when it can
129
+ // be resolved and the allow-list (if any) permits it.
130
+ try {
131
+ const fallback = resolveSenderAddress(config, userEmail);
132
+ if (isSenderAllowed(fallback, patterns))
133
+ add(fallback.toLowerCase());
134
+ }
135
+ catch {
136
+ // No default resolvable (e.g. file mode with no user / domain / from).
137
+ }
138
+ for (const pattern of patterns) {
139
+ if (pattern === "*")
140
+ continue; // "any" can't be enumerated as a choice
141
+ if (pattern.startsWith("*@") || !pattern.includes("@")) {
142
+ const domain = pattern.startsWith("*@") ? pattern.slice(2) : pattern;
143
+ if (local)
144
+ add(`${local}@${domain}`);
145
+ }
146
+ else {
147
+ add(pattern); // exact address
148
+ }
149
+ }
150
+ return options;
151
+ }
152
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VuZGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3NlbmRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBcUJHO0FBRUgsT0FBTyxFQUFFLGtCQUFrQixFQUFFLGVBQWUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ3pFLE9BQU8sRUFBRSxHQUFHLEVBQUUsR0FBRyxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFJbEQsTUFBTSxNQUFNLEdBQUcsR0FBRyxDQUFDLE1BQU0sQ0FBQyxjQUFjLENBQUMsQ0FBQztBQUUxQzs7O0dBR0c7QUFDSCxNQUFNLFVBQVUsbUJBQW1CLENBQUMsU0FBNkIsRUFBRSxNQUFjO0lBQy9FLE1BQU0sS0FBSyxHQUFHLFNBQVMsRUFBRSxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsSUFBSSxFQUFFLENBQUM7SUFDL0MsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ1gsTUFBTSxrQkFBa0IsQ0FBQyxnQkFBZ0IsQ0FDdkMseUJBQXlCLEVBQ3pCLCtFQUErRSxDQUNoRixDQUFDO0lBQ0osQ0FBQztJQUNELE9BQU8sR0FBRyxLQUFLLElBQUksTUFBTSxFQUFFLENBQUM7QUFDOUIsQ0FBQztBQUVEOzs7Ozs7OztHQVFHO0FBQ0gsTUFBTSxVQUFVLG1CQUFtQixDQUFDLEdBQWtDO0lBQ3BFLE9BQU8sR0FBRyxDQUFDLFdBQVcsQ0FBQyxHQUFHLEVBQUUsRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztBQUNuRCxDQUFDO0FBRUQsZ0ZBQWdGO0FBQ2hGLFNBQVMsbUJBQW1CLENBQUMsT0FBZTtJQUMxQyxJQUFJLE9BQU8sQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDO1FBQUUsT0FBTyxJQUFJLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUM1RCxJQUFJLENBQUMsT0FBTyxDQUFDLFFB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RUFBRSxTQUFTLENBQUMsQ0FBQztRQUN6RCxJQUFJLGVBQWUsQ0FBQyxRQUFRLEVBQUUsUUFBUSxDQUFDO1lBQUUsR0FBRyxDQUFDLFFBQVEsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDO0lBQ3ZFLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCx1RUFBdUU7SUFDekUsQ0FBQztJQUVELEtBQUssTUFBTSxPQUFPLElBQUksUUFBUSxFQUFFLENBQUM7UUFDL0IsSUFBSSxPQUFPLEtBQUssR0FBRztZQUFFLFNBQVMsQ0FBQyx3Q0FBd0M7UUFDdkUsSUFBSSxPQUFPLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDO1lBQ3ZELE1BQU0sTUFBTSxHQUFHLE9BQU8sQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQztZQUNyRSxJQUFJLEtBQUs7Z0JBQUUsR0FBRyxDQUFDLEdBQUcsS0FBSyxJQUFJLE1BQU0sRUFBRSxDQUFDLENBQUM7UUFDdkMsQ0FBQzthQUFNLENBQUM7WUFDTixHQUFHLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxnQkFBZ0I7UUFDaEMsQ0FBQztJQUNILENBQUM7SUFDRCxPQUFPLE9BQU8sQ0FBQztBQUNqQixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTZW5kZXItYWRkcmVzcyBwb2xpY3k6IHR1cm4gdGhlIG9uLWJlaGFsZi1vZiB1c2VyJ3MgZW1haWwgaW50byBhblxuICogb3V0Ym91bmQgYEZyb21gLCBhbmQgKG9wdGlvbmFsbHkpIHJlc3RyaWN0IHdoaWNoIGFkZHJlc3NlcyBtYXkgc2VuZC5cbiAqXG4gKiBUaGUgZGVmYXVsdCBgRnJvbWAgcmUtaG9tZXMgdGhlIGxvY2FsIHBhcnQgKGV2ZXJ5dGhpbmcgYmVmb3JlIGBAYCkgb2ZcbiAqIHRoZSBPQk8gZW1haWwgb24gdGhlIGNvbmZpZ3VyZWQgc2VuZGluZyBkb21haW4sIHNvIGBhbGljZUBkYXRhYnJpY2tzLmNvbWBcbiAqIHRocm91Z2ggYSBkb21haW4gb2YgYG1haWwuZXhhbXBsZS5jb21gIGdvZXMgb3V0IGFzXG4gKiBgYWxpY2VAbWFpbC5leGFtcGxlLmNvbWAuIEFuIGV4cGxpY2l0IGBmcm9tYCBzaG9ydC1jaXJjdWl0cyB0aGF0OyB0aGVcbiAqIGZpbGUvb3V0Ym94IGZhbGxiYWNrIChubyBkb21haW4pIGtlZXBzIHRoZSB1c2VyJ3MgYWRkcmVzcyB2ZXJiYXRpbSBzb1xuICogdGVzdCBhcnRpZmFjdHMgbGFuZCB1bmRlciBhIHJlY29nbml6YWJsZSBmb2xkZXIuXG4gKlxuICogVGhlIHJlc29sdmVkIGBGcm9tYCBpcyB0aGVuIGNvbnN0cmFpbmVkIHRvIHRoZSBlZmZlY3RpdmUgYWxsb3ctbGlzdDogYVxuICogcGF0dGVybiBpcyBlaXRoZXIgYW4gZXhhY3QgYWRkcmVzcyAoYHVzZXJAZG9tYWluLmNvbWApLCBhIGRvbWFpbiB3aWxkY2FyZFxuICogKGAqQGRvbWFpbi5jb21gIG9yIHRoZSBiYXJlIGBkb21haW4uY29tYCwgbWF0Y2hpbmcgYW55IGxvY2FsIHBhcnQgb24gdGhhdFxuICogZG9tYWluKSwgb3IgYCpgIChhbnkpLiBUaGlzIG1vZHVsZSBvbmx5IG1hdGNoZXMgcGF0dGVybnM7IHdoaWNoIHBhdHRlcm5zXG4gKiBhcHBseSBpcyBkZWNpZGVkIGJ5IHRoZSBjb25maWd1cmVkIHNlbmRlciBwb2xpY3kgaW4gYC4vY29uZmlnYCwgd2hpY2hcbiAqIHVuZGVyIHRoZSBkZWZhdWx0IGBcImFsbG93bGlzdFwiYCBtb2RlIGZpbGxzIGFuIGVtcHR5IGxpc3QgaW4gZnJvbSB0aGVcbiAqIHNlbmRlciBzb3VyY2UuIHtAbGluayBsaXN0U2VuZGVyT3B0aW9uc30gZXhwYW5kcyB0aGUgZWZmZWN0aXZlIGxpc3QgaW50b1xuICogdGhlIGNvbmNyZXRlIGFkZHJlc3NlcyBhIFVJIGRyb3Bkb3duIGNhbiBvZmZlciBmb3IgdGhlIGN1cnJlbnQgdXNlci5cbiAqXG4gKiBAbW9kdWxlXG4gKi9cblxuaW1wb3J0IHsgQ29uZmlndXJhdGlvbkVycm9yLCBWYWxpZGF0aW9uRXJyb3IgfSBmcm9tIFwiQGRhdGFicmlja3MvYXBwa2l0XCI7XG5pbXBvcnQgeyBsb2csIG5ldCB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlXCI7XG5cbmltcG9ydCB0eXBlIHsgUmVzb2x2ZWRFbWFpbENvbmZpZyB9IGZyb20gXCIuL2NvbmZpZ1wiO1xuXG5jb25zdCBsb2dnZXIgPSBsb2cubG9nZ2VyKFwiZW1haWwvc2VuZGVyXCIpO1xuXG4vKipcbiAqIFJlLWhvbWUgdGhlIE9CTyB1c2VyJ3MgbG9jYWwgcGFydCBvbiBgZG9tYWluYC4gVGhyb3dzIHdoZW4gbm8gdXNhYmxlXG4gKiBsb2NhbCBwYXJ0IGlzIGF2YWlsYWJsZSAoZS5nLiBhIHNlcnZpY2UtY29udGV4dCBjYWxsIHdpdGggbm8gdXNlcikuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBkZXJpdmVTZW5kZXJBZGRyZXNzKHVzZXJFbWFpbDogc3RyaW5nIHwgdW5kZWZpbmVkLCBkb21haW46IHN0cmluZyk6IHN0cmluZyB7XG4gIGNvbnN0IGxvY2FsID0gdXNlckVtYWlsPy5zcGxpdChcIkBcIilbMF0/LnRyaW0oKTtcbiAgaWYgKCFsb2NhbCkge1xuICAgIHRocm93IENvbmZpZ3VyYXRpb25FcnJvci5yZXNvdXJjZU5vdEZvdW5kKFxuICAgICAgXCJPbi1iZWhhbGYtb2YgdXNlciBlbWFpbFwiLFxuICAgICAgXCJTZXQgYGZyb21gIC8gRU1BSUxfRlJPTSB0byBzZW5kIGZyb20gYSBmaXhlZCBhZGRyZXNzIGluc3RlYWQgb2YgZGVyaXZpbmcgb25lLlwiLFxuICAgICk7XG4gIH1cbiAgcmV0dXJuIGAke2xvY2FsfUAke2RvbWFpbn1gO1xufVxuXG4vKipcbiAqIE5vcm1hbGl6ZSBhIHNlbmRlciBhbGxvdy1saXN0IGZyb20gY29uZmlnIChhIGBzdHJpbmdbXWApIG9yIGFuIGVudiB2YXJcbiAqIChhIENTViAvIHdoaXRlc3BhY2Utc2VwYXJhdGVkIHN0cmluZykuIERlbGVnYXRlcyB0byB0aGUgc2hhcmVkXG4gKiB7QGxpbmsgbmV0LnBhcnNlRW1haWxzfSBzbyBhbGxvdy1saXN0IHBhdHRlcm5zIGFyZSByZWFkIGV4YWN0bHlcbiAqIGxpa2UgcmVjaXBpZW50IGxpc3RzIGVsc2V3aGVyZTogZW50cmllcyBhcmUgdHJpbW1lZCwgbG93ZXItY2FzZWQgKHNvXG4gKiBtYXRjaGluZyBpbiB7QGxpbmsgaXNTZW5kZXJBbGxvd2VkfSBpcyBjYXNlLWluc2Vuc2l0aXZlKSwgYW5kXG4gKiBkZS1kdXBsaWNhdGVkOyBlbXB0aWVzIGFyZSBkcm9wcGVkLiBBbiBlbXB0eSByZXN1bHQgbWVhbnMgXCJub1xuICogcmVzdHJpY3Rpb25cIi5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHBhcnNlQWxsb3dlZFNlbmRlcnMocmF3OiBzdHJpbmcgfCBzdHJpbmdbXSB8IHVuZGVmaW5lZCk6IHN0cmluZ1tdIHtcbiAgcmV0dXJuIG5ldC5wYXJzZUVtYWlscyhyYXcsIHsgbG93ZXJjYXNlOiB0cnVlIH0pO1xufVxuXG4vKiogVGhlIGBAZG9tYWluYCBzdWZmaXggYSB3aWxkY2FyZCAvIGJhcmUtZG9tYWluIHBhdHRlcm4gbWF0Y2hlcywgZWxzZSBudWxsLiAqL1xuZnVuY3Rpb24gcGF0dGVybkRvbWFpblN1ZmZpeChwYXR0ZXJuOiBzdHJpbmcpOiBzdHJpbmcgfCBudWxsIHtcbiAgaWYgKHBhdHRlcm4uc3RhcnRzV2l0aChcIipAXCIpKSByZXR1cm4gYEAke3BhdHRlcm4uc2xpY2UoMil9YDtcbiAgaWYgKCFwYXR0ZXJuLmluY2x1ZGVzKFwiQFwiKSkgcmV0dXJuIGBAJHtwYXR0ZXJufWA7XG4gIHJldHVybiBudWxsO1xufVxuXG4vKiogV2hldGhlciBgYWRkcmVzc2AgKGFscmVhZHkgbG93ZXItY2FzZWQpIHNhdGlzZmllcyBhIHNpbmdsZSBwYXR0ZXJuLiAqL1xuZnVuY3Rpb24gbWF0Y2hlc1BhdHRlcm4oYWRkcmVzczogc3RyaW5nLCBwYXR0ZXJuOiBzdHJpbmcpOiBib29sZWFuIHtcbiAgaWYgKHBhdHRlcm4gPT09IFwiKlwiKSByZXR1cm4gdHJ1ZTtcbiAgY29uc3Qgc3VmZml4ID0gcGF0dGVybkRvbWFpblN1ZmZpeChwYXR0ZXJuKTtcbiAgaWYgKHN1ZmZpeCkgcmV0dXJuIGFkZHJlc3MubGVuZ3RoID4gc3VmZml4Lmxlbmd0aCAmJiBhZGRyZXNzLmVuZHNXaXRoKHN1ZmZpeCk7XG4gIHJldHVybiBhZGRyZXNzID09PSBwYXR0ZXJuO1xufVxuXG4vKipcbiAqIFdoZXRoZXIgYGZyb21gIGlzIHBlcm1pdHRlZCBieSB0aGUgYWxsb3ctbGlzdC4gQW4gZW1wdHkgKG9yIGFic2VudClcbiAqIGFsbG93LWxpc3QgcGVybWl0cyBldmVyeXRoaW5nOiB7QGxpbmsgcmVzb2x2ZUVtYWlsQ29uZmlnfSBpcyB3aGF0IHR1cm5zXG4gKiB0aGUgY29uZmlndXJlZCB7QGxpbmsgU2VuZGVyUG9saWN5fSBpbnRvIGNvbmNyZXRlIHBhdHRlcm5zLCBzbyBhbiBlbXB0eVxuICogbGlzdCBoZXJlIG1lYW5zIHRoZSBwb2xpY3kgaGFkIG5vdGhpbmcgdG8gbmFycm93IHRvLlxuICovXG5leHBvcnQgZnVuY3Rpb24gaXNTZW5kZXJBbGxvd2VkKGZyb206IHN0cmluZywgcGF0dGVybnM6IHN0cmluZ1tdKTogYm9vbGVhbiB7XG4gIGlmIChwYXR0ZXJucy5sZW5ndGggPT09IDApIHJldHVybiB0cnVlO1xuICBjb25zdCBhZGRyZXNzID0gZnJvbS50cmltKCkudG9Mb3dlckNhc2UoKTtcbiAgcmV0dXJuIHBhdHRlcm5zLnNvbWUoKHBhdHRlcm4pID0+IG1hdGNoZXNQYXR0ZXJuKGFkZHJlc3MsIHBhdHRlcm4pKTtcbn1cblxuLyoqXG4gKiBUaHJvdyB3aGVuIGBmcm9tYCBpcyBub3QgcGVybWl0dGVkIGJ5IHRoZSBhbGxvdy1saXN0LiBOby1vcCB3aGVuIHRoZVxuICogYWxsb3ctbGlzdCBpcyBlbXB0eS4gVGhlIHNpbmdsZSBlbmZvcmNlbWVudCBwb2ludCBmb3IgdGhlIHJlc3RyaWN0aW9uXG4gKiAoY2FsbGVkIGZyb20ge0BsaW5rIHNlbmRFbWFpbH0pLCBzbyBldmVyeSBzZW5kIHBhdGggaXMgY292ZXJlZCB3aGV0aGVyXG4gKiB0aGUgYWRkcmVzcyB3YXMgZGVyaXZlZCBzZXJ2ZXItc2lkZSBvciBjaG9zZW4gaW4gYSBVSS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGFzc2VydFNlbmRlckFsbG93ZWQoZnJvbTogc3RyaW5nLCBwYXR0ZXJuczogc3RyaW5nW10pOiB2b2lkIHtcbiAgaWYgKGlzU2VuZGVyQWxsb3dlZChmcm9tLCBwYXR0ZXJucykpIHJldHVybjtcbiAgLy8gVGhlIHRocm93biBtZXNzYWdlIG5hbWVzIHRoZSBmaWVsZCBvbmx5OyB0aGUgcGF0dGVybnMgYXJlIHBvbGljeSBkZXRhaWxcbiAgLy8gdGhhdCBiZWxvbmdzIGluIHRoZSBvcGVyYXRvcidzIGxvZ3MsIG5vdCBpbiBhIGNsaWVudCBvciBtb2RlbCByZXNwb25zZS5cbiAgbG9nZ2VyLndhcm4oXCJzZW5kZXI6ZGVuaWVkXCIsIHsgZnJvbSwgYWxsb3dlZFNlbmRlcnM6IHBhdHRlcm5zIH0pO1xuICB0aHJvdyBWYWxpZGF0aW9uRXJyb3IuaW52YWxpZFZhbHVlKFxuICAgIFwiZnJvbVwiLFxuICAgIGZyb20sXG4gICAgXCJhbiBhZGRyZXNzIHBlcm1pdHRlZCBieSB0aGUgY29uZmlndXJlZCBzZW5kZXIgYWxsb3ctbGlzdFwiLFxuICApO1xufVxuXG4vKipcbiAqIFJlc29sdmUgdGhlIGBGcm9tYCBhZGRyZXNzIGZvciBhIHNlbmQgZnJvbSB0aGUgcmVzb2x2ZWQgY29uZmlnIGFuZCB0aGVcbiAqIGN1cnJlbnQgT0JPIHVzZXI6IGV4cGxpY2l0IGBmcm9tYCB3aW5zLCB0aGVuIGA8bG9jYWw+QDxkb21haW4+YCwgdGhlblxuICogKGZpbGUvb3V0Ym94IG1vZGUgb25seSkgdGhlIHVzZXIncyBlbWFpbCB2ZXJiYXRpbS4gVGhyb3dzIHdoZW4gbm9uZSBvZlxuICogdGhvc2UgeWllbGQgYW4gYWRkcmVzcy5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlc29sdmVTZW5kZXJBZGRyZXNzKFxuICBjb25maWc6IFJlc29sdmVkRW1haWxDb25maWcsXG4gIHVzZXJFbWFpbDogc3RyaW5nIHwgdW5kZWZpbmVkLFxuKTogc3RyaW5nIHtcbiAgaWYgKGNvbmZpZy5mcm9tKSByZXR1cm4gY29uZmlnLmZyb207XG4gIGlmIChjb25maWcuZG9tYWluKSByZXR1cm4gZGVyaXZlU2VuZGVyQWRkcmVzcyh1c2VyRW1haWwsIGNvbmZpZy5kb21haW4pO1xuICBjb25zdCBlbWFpbCA9IHVzZXJFbWFpbD8udHJpbSgpO1xuICBpZiAoIWVtYWlsKSB7XG4gICAgdGhyb3cgQ29uZmlndXJhdGlvbkVycm9yLnJlc291cmNlTm90Rm91bmQoXG4gICAgICBcIkVtYWlsIHNlbmRlciBhZGRyZXNzXCIsXG4gICAgICBcIlNldCBgZnJvbWAgLyBFTUFJTF9GUk9NLCBzZXQgYGRvbWFpbmAgLyBFTUFJTF9ET01BSU4sIG9yIHJ1biBvbiBiZWhhbGYgb2YgYSB1c2VyLlwiLFxuICAgICk7XG4gIH1cbiAgcmV0dXJuIGVtYWlsO1xufVxuXG4vKipcbiAqIEV4cGFuZCB0aGUgcmVzb2x2ZWQgY29uZmlnJ3MgYWxsb3ctbGlzdCBpbnRvIHRoZSBjb25jcmV0ZSBgRnJvbWBcbiAqIGFkZHJlc3NlcyBvZmZlcmVkIHRvIHRoZSBjdXJyZW50IHVzZXIgLSB0aGUgZGF0YSBhIFVJIHNlbmRlciBkcm9wZG93blxuICogcmVuZGVycy4gRXhhY3QtYWRkcmVzcyBwYXR0ZXJucyBwYXNzIHRocm91Z2g7IGRvbWFpbiB3aWxkY2FyZHNcbiAqIChgKkBkb21haW4uY29tYCAvIGJhcmUgYGRvbWFpbi5jb21gKSBhcmUgY29uY3JldGl6ZWQgYXNcbiAqIGA8dXNlci1sb2NhbD5APGRvbWFpbj5gIGFuZCBkcm9wcGVkIHdoZW4gbm8gT0JPIHVzZXIgbG9jYWwgcGFydCBpc1xuICogYXZhaWxhYmxlLiBXaGVuIG5vIGFsbG93LWxpc3QgaXMgY29uZmlndXJlZCwgdGhlIHNpbmdsZSBkZWZhdWx0IHNlbmRlclxuICogKHtAbGluayByZXNvbHZlU2VuZGVyQWRkcmVzc30pIGlzIHJldHVybmVkIHdoZW4gaXQgY2FuIGJlIHJlc29sdmVkLFxuICogZWxzZSBhbiBlbXB0eSBsaXN0LiBUaGUgZGVmYXVsdCByZXNvbHZlZCBzZW5kZXIsIHdoZW4gcGVybWl0dGVkLCBpc1xuICogb3JkZXJlZCBmaXJzdC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGxpc3RTZW5kZXJPcHRpb25zKFxuICBjb25maWc6IFJlc29sdmVkRW1haWxDb25maWcsXG4gIHVzZXJFbWFpbDogc3RyaW5nIHwgdW5kZWZpbmVkLFxuKTogc3RyaW5nW10ge1xuICBjb25zdCBwYXR0ZXJucyA9IGNvbmZpZy5hbGxvd2VkU2VuZGVycyA/PyBbXTtcbiAgY29uc3QgbG9jYWwgPSB1c2VyRW1haWw/LnNwbGl0KFwiQFwiKVswXT8udHJpbSgpLnRvTG93ZXJDYXNlKCk7XG4gIGNvbnN0IG9wdGlvbnM6IHN0cmluZ1tdID0gW107XG4gIGNvbnN0IGFkZCA9IChhZGRyZXNzOiBzdHJpbmcgfCB1bmRlZmluZWQpOiB2b2lkID0+IHtcbiAgICBpZiAoYWRkcmVzcyAmJiAhb3B0aW9ucy5pbmNsdWRlcyhhZGRyZXNzKSkgb3B0aW9ucy5wdXNoKGFkZHJlc3MpO1xuICB9O1xuXG4gIC8vIFN1cmZhY2UgdGhlIGFkZHJlc3MgYSBzZW5kIHdvdWxkIHVzZSBieSBkZWZhdWx0IGZpcnN0LCB3aGVuIGl0IGNhblxuICAvLyBiZSByZXNvbHZlZCBhbmQgdGhlIGFsbG93LWxpc3QgKGlmIGFueSkgcGVybWl0cyBpdC5cbiAgdHJ5IHtcbiAgICBjb25zdCBmYWxsYmFjayA9IHJlc29sdmVTZW5kZXJBZGRyZXNzKGNvbmZpZywgdXNlckVtYWlsKTtcbiAgICBpZiAoaXNTZW5kZXJBbGxvd2VkKGZhbGxiYWNrLCBwYXR0ZXJucykpIGFkZChmYWxsYmFjay50b0xvd2VyQ2FzZSgpKTtcbiAgfSBjYXRjaCB7XG4gICAgLy8gTm8gZGVmYXVsdCByZXNvbHZhYmxlIChlLmcuIGZpbGUgbW9kZSB3aXRoIG5vIHVzZXIgLyBkb21haW4gLyBmcm9tKS5cbiAgfVxuXG4gIGZvciAoY29uc3QgcGF0dGVybiBvZiBwYXR0ZXJucykge1xuICAgIGlmIChwYXR0ZXJuID09PSBcIipcIikgY29udGludWU7IC8vIFwiYW55XCIgY2FuJ3QgYmUgZW51bWVyYXRlZCBhcyBhIGNob2ljZVxuICAgIGlmIChwYXR0ZXJuLnN0YXJ0c1dpdGgoXCIqQFwiKSB8fCAhcGF0dGVybi5pbmNsdWRlcyhcIkBcIikpIHtcbiAgICAgIGNvbnN0IGRvbWFpbiA9IHBhdHRlcm4uc3RhcnRzV2l0aChcIipAXCIpID8gcGF0dGVybi5zbGljZSgyKSA6IHBhdHRlcm47XG4gICAgICBpZiAobG9jYWwpIGFkZChgJHtsb2NhbH1AJHtkb21haW59YCk7XG4gICAgfSBlbHNlIHtcbiAgICAgIGFkZChwYXR0ZXJuKTsgLy8gZXhhY3QgYWRkcmVzc1xuICAgIH1cbiAgfVxuICByZXR1cm4gb3B0aW9ucztcbn1cbiJdfQ==
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The `send_email` Mastra tool: approval-gated so a model can draft a
3
+ * message freely but nothing leaves the building until a human clicks
4
+ * Approve in the chat UI. On approval the sender is resolved (explicit
5
+ * `from` config, else derived from the on-behalf-of user's email) and
6
+ * the message is dispatched through the shared SMTP transport.
7
+ *
8
+ * The sender derivation runs inside the AppKit user scope, so
9
+ * `getExecutionContext()` returns the OBO user whose local-part seeds
10
+ * the address (see {@link deriveSenderAddress}).
11
+ *
12
+ * The dispatch itself goes through the executor the plugin installs on the
13
+ * shared runtime, so a send from this tool picks up the same retry / timeout /
14
+ * telemetry chain as one from the AppKit tool. In a Mastra app with no AppKit
15
+ * plugin registered the send still runs, just without interceptors.
16
+ *
17
+ * @module
18
+ */
19
+ /**
20
+ * The model-facing description of the send capability, shared by the Mastra
21
+ * {@link emailTool} and the AppKit `email.send` tool so both agents get the
22
+ * same guidance about approval, scope, and body formatting.
23
+ */
24
+ export declare const SEND_EMAIL_DESCRIPTION: string;
25
+ /** Options accepted by {@link emailTool}. */
26
+ export interface EmailToolOptions {
27
+ /**
28
+ * Override the tool id. Defaults to `"send_email"`; the chat UI's
29
+ * approval gate keys off this id, so keep it unless you also teach
30
+ * the client about the new name.
31
+ */
32
+ id?: string;
33
+ }
34
+ /**
35
+ * Build the approval-gated `send_email` tool. Spread it into the agents
36
+ * that should be able to draft mail; it is intentionally not installed
37
+ * everywhere.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import { emailTool } from "@dbx-tools/email";
42
+ * import { createAgent } from "@dbx-tools/appkit-mastra";
43
+ *
44
+ * const support = createAgent({
45
+ * instructions: "...",
46
+ * tools: () => ({ send_email: emailTool() }),
47
+ * });
48
+ * ```
49
+ */
50
+ export declare function emailTool(opts?: EmailToolOptions): import("@mastra/core/tools").Tool<{
51
+ to: string[];
52
+ subject: string;
53
+ body: string;
54
+ cc?: string[] | undefined;
55
+ bcc?: string[] | undefined;
56
+ attachments?: {
57
+ filename: string;
58
+ content?: string | undefined;
59
+ encoding?: string | undefined;
60
+ path?: string | undefined;
61
+ contentType?: string | undefined;
62
+ }[] | undefined;
63
+ }, {
64
+ sent: boolean;
65
+ recipient: string;
66
+ from: string;
67
+ messageId?: string | undefined;
68
+ }, unknown, unknown, import("@mastra/core/tools").ToolExecutionContext<unknown, unknown, unknown>, string, unknown>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The `send_email` Mastra tool: approval-gated so a model can draft a
3
+ * message freely but nothing leaves the building until a human clicks
4
+ * Approve in the chat UI. On approval the sender is resolved (explicit
5
+ * `from` config, else derived from the on-behalf-of user's email) and
6
+ * the message is dispatched through the shared SMTP transport.
7
+ *
8
+ * The sender derivation runs inside the AppKit user scope, so
9
+ * `getExecutionContext()` returns the OBO user whose local-part seeds
10
+ * the address (see {@link deriveSenderAddress}).
11
+ *
12
+ * The dispatch itself goes through the executor the plugin installs on the
13
+ * shared runtime, so a send from this tool picks up the same retry / timeout /
14
+ * telemetry chain as one from the AppKit tool. In a Mastra app with no AppKit
15
+ * plugin registered the send still runs, just without interceptors.
16
+ *
17
+ * @module
18
+ */
19
+ import { getExecutionContext } from "@databricks/appkit";
20
+ import { log, string } from "@dbx-tools/shared-core";
21
+ import { email } from "@dbx-tools/shared-email";
22
+ import { createTool } from "@mastra/core/tools";
23
+ import { resolveSenderAddress } from "./sender.js";
24
+ import { getEmailRuntime, sendEmail } from "./transport.js";
25
+ const logger = log.logger("email/tool/send-email");
26
+ /**
27
+ * The model-facing description of the send capability, shared by the Mastra
28
+ * {@link emailTool} and the AppKit `email.send` tool so both agents get the
29
+ * same guidance about approval, scope, and body formatting.
30
+ */
31
+ export const SEND_EMAIL_DESCRIPTION = string.toDescription(`
32
+ Send an email on the user's behalf. Pass one or more recipient
33
+ addresses (with optional cc / bcc and file attachments), a subject,
34
+ and a body; the user is prompted to approve the send before it goes
35
+ out (this tool is approval-gated). Use it only when the user
36
+ explicitly asks to send / forward / share something via email -
37
+ never autonomously. Keep subjects short and bodies self-contained:
38
+ the recipient has none of the chat context. Write the body in
39
+ GitHub-Flavored Markdown - headings, lists, and real Markdown
40
+ tables - not ASCII art (no "=====" dividers or space/pipe-drawn
41
+ tables); it is rendered to HTML before sending.
42
+ `);
43
+ /**
44
+ * Build the approval-gated `send_email` tool. Spread it into the agents
45
+ * that should be able to draft mail; it is intentionally not installed
46
+ * everywhere.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * import { emailTool } from "@dbx-tools/email";
51
+ * import { createAgent } from "@dbx-tools/appkit-mastra";
52
+ *
53
+ * const support = createAgent({
54
+ * instructions: "...",
55
+ * tools: () => ({ send_email: emailTool() }),
56
+ * });
57
+ * ```
58
+ */
59
+ export function emailTool(opts = {}) {
60
+ return createTool({
61
+ id: opts.id ?? "send_email",
62
+ description: SEND_EMAIL_DESCRIPTION,
63
+ inputSchema: email.emailMessageSchema,
64
+ outputSchema: email.emailResultSchema,
65
+ requireApproval: true,
66
+ execute: async (input, context) => {
67
+ const message = email.emailMessageSchema.parse(input);
68
+ const { config } = getEmailRuntime();
69
+ const ctx = getExecutionContext();
70
+ const userEmail = "isUserContext" in ctx ? ctx.userEmail : undefined;
71
+ const from = resolveSenderAddress(config, userEmail);
72
+ const result = await sendEmail(message, from, context?.abortSignal);
73
+ logger.info("sent", {
74
+ to: result.recipient,
75
+ from: result.from,
76
+ ...(result.messageId ? { messageId: result.messageId } : {}),
77
+ });
78
+ return result;
79
+ },
80
+ });
81
+ }
82
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9vbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90b29sLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7OztHQWlCRztBQUVILE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ3pELE9BQU8sRUFBRSxHQUFHLEVBQUUsTUFBTSxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDckQsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ2hELE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUNoRCxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsTUFBTSxVQUFVLENBQUM7QUFDaEQsT0FBTyxFQUFFLGVBQWUsRUFBRSxTQUFTLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFFekQsTUFBTSxNQUFNLEdBQUcsR0FBRyxDQUFDLE1BQU0sQ0FBQyx1QkFBdUIsQ0FBQyxDQUFDO0FBRW5EOzs7O0dBSUc7QUFDSCxNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBRyxNQUFNLENBQUMsYUFBYSxDQUFDOzs7Ozs7Ozs7OztDQVcxRCxDQUFDLENBQUM7QUFZSDs7Ozs7Ozs7Ozs7Ozs7O0dBZUc7QUFDSCxNQUFNLFVBQVUsU0FBUyxDQUFDLE9BQXlCLEVBQUU7SUFDbkQsT0FBTyxVQUFVLENBQUM7UUFDaEIsRUFBRSxFQUFFLElBQUksQ0FBQyxFQUFFLElBQUksWUFBWTtRQUMzQixXQUFXLEVBQUUsc0JBQXNCO1FBQ25DLFdBQVcsRUFBRSxLQUFLLENBQUMsa0JBQWtCO1FBQ3JDLFlBQVksRUFBRSxLQUFLLENBQUMsaUJBQWlCO1FBQ3JDLGVBQWUsRUFBRSxJQUFJO1FBQ3JCLE9BQU8sRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO1lBQ2hDLE1BQU0sT0FBTyxHQUFHLEtBQUssQ0FBQyxrQkFBa0IsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLENBQUM7WUFDdEQsTUFBTSxFQUFFLE1BQU0sRUFBRSxHQUFHLGVBQWUsRUFBRSxDQUFDO1lBQ3JDLE1BQU0sR0FBRyxHQUFHLG1CQUFtQixFQUFFLENBQUM7WUFDbEMsTUFBTSxTQUFTLEdBQUcsZUFBZSxJQUFJLEdBQUcsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDO1lBQ3JFLE1BQU0sSUFBSSxHQUFHLG9CQUFvQixDQUFDLE1BQU0sRUFBRSxTQUFTLENBQUMsQ0FBQztZQUNyRCxNQUFNLE1BQU0sR0FBRyxNQUFNLFNBQVMsQ0FBQyxPQUFPLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBRSxXQUFXLENBQUMsQ0FBQztZQUNwRSxNQUFNLENBQUMsSUFBSSxDQUFDLE1BQU0sRUFBRTtnQkFDbEIsRUFBRSxFQUFFLE1BQU0sQ0FBQyxTQUFTO2dCQUNwQixJQUFJLEVBQUUsTUFBTSxDQUFDLElBQUk7Z0JBQ2pCLEdBQUcsQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxFQUFFLFNBQVMsRUFBRSxNQUFNLENBQUMsU0FBUyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQzthQUM3RCxDQUFDLENBQUM7WUFDSCxPQUFPLE1BQU0sQ0FBQztRQUNoQixDQUFDO0tBQ0YsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogVGhlIGBzZW5kX2VtYWlsYCBNYXN0cmEgdG9vbDogYXBwcm92YWwtZ2F0ZWQgc28gYSBtb2RlbCBjYW4gZHJhZnQgYVxuICogbWVzc2FnZSBmcmVlbHkgYnV0IG5vdGhpbmcgbGVhdmVzIHRoZSBidWlsZGluZyB1bnRpbCBhIGh1bWFuIGNsaWNrc1xuICogQXBwcm92ZSBpbiB0aGUgY2hhdCBVSS4gT24gYXBwcm92YWwgdGhlIHNlbmRlciBpcyByZXNvbHZlZCAoZXhwbGljaXRcbiAqIGBmcm9tYCBjb25maWcsIGVsc2UgZGVyaXZlZCBmcm9tIHRoZSBvbi1iZWhhbGYtb2YgdXNlcidzIGVtYWlsKSBhbmRcbiAqIHRoZSBtZXNzYWdlIGlzIGRpc3BhdGNoZWQgdGhyb3VnaCB0aGUgc2hhcmVkIFNNVFAgdHJhbnNwb3J0LlxuICpcbiAqIFRoZSBzZW5kZXIgZGVyaXZhdGlvbiBydW5zIGluc2lkZSB0aGUgQXBwS2l0IHVzZXIgc2NvcGUsIHNvXG4gKiBgZ2V0RXhlY3V0aW9uQ29udGV4dCgpYCByZXR1cm5zIHRoZSBPQk8gdXNlciB3aG9zZSBsb2NhbC1wYXJ0IHNlZWRzXG4gKiB0aGUgYWRkcmVzcyAoc2VlIHtAbGluayBkZXJpdmVTZW5kZXJBZGRyZXNzfSkuXG4gKlxuICogVGhlIGRpc3BhdGNoIGl0c2VsZiBnb2VzIHRocm91Z2ggdGhlIGV4ZWN1dG9yIHRoZSBwbHVnaW4gaW5zdGFsbHMgb24gdGhlXG4gKiBzaGFyZWQgcnVudGltZSwgc28gYSBzZW5kIGZyb20gdGhpcyB0b29sIHBpY2tzIHVwIHRoZSBzYW1lIHJldHJ5IC8gdGltZW91dCAvXG4gKiB0ZWxlbWV0cnkgY2hhaW4gYXMgb25lIGZyb20gdGhlIEFwcEtpdCB0b29sLiBJbiBhIE1hc3RyYSBhcHAgd2l0aCBubyBBcHBLaXRcbiAqIHBsdWdpbiByZWdpc3RlcmVkIHRoZSBzZW5kIHN0aWxsIHJ1bnMsIGp1c3Qgd2l0aG91dCBpbnRlcmNlcHRvcnMuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbmltcG9ydCB7IGdldEV4ZWN1dGlvbkNvbnRleHQgfSBmcm9tIFwiQGRhdGFicmlja3MvYXBwa2l0XCI7XG5pbXBvcnQgeyBsb2csIHN0cmluZyB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlXCI7XG5pbXBvcnQgeyBlbWFpbCB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1lbWFpbFwiO1xuaW1wb3J0IHsgY3JlYXRlVG9vbCB9IGZyb20gXCJAbWFzdHJhL2NvcmUvdG9vbHNcIjtcbmltcG9ydCB7IHJlc29sdmVTZW5kZXJBZGRyZXNzIH0gZnJvbSBcIi4vc2VuZGVyXCI7XG5pbXBvcnQgeyBnZXRFbWFpbFJ1bnRpbWUsIHNlbmRFbWFpbCB9IGZyb20gXCIuL3RyYW5zcG9ydFwiO1xuXG5jb25zdCBsb2dnZXIgPSBsb2cubG9nZ2VyKFwiZW1haWwvdG9vbC9zZW5kLWVtYWlsXCIpO1xuXG4vKipcbiAqIFRoZSBtb2RlbC1mYWNpbmcgZGVzY3JpcHRpb24gb2YgdGhlIHNlbmQgY2FwYWJpbGl0eSwgc2hhcmVkIGJ5IHRoZSBNYXN0cmFcbiAqIHtAbGluayBlbWFpbFRvb2x9IGFuZCB0aGUgQXBwS2l0IGBlbWFpbC5zZW5kYCB0b29sIHNvIGJvdGggYWdlbnRzIGdldCB0aGVcbiAqIHNhbWUgZ3VpZGFuY2UgYWJvdXQgYXBwcm92YWwsIHNjb3BlLCBhbmQgYm9keSBmb3JtYXR0aW5nLlxuICovXG5leHBvcnQgY29uc3QgU0VORF9FTUFJTF9ERVNDUklQVElPTiA9IHN0cmluZy50b0Rlc2NyaXB0aW9uKGBcbiAgU2VuZCBhbiBlbWFpbCBvbiB0aGUgdXNlcidzIGJlaGFsZi4gUGFzcyBvbmUgb3IgbW9yZSByZWNpcGllbnRcbiAgYWRkcmVzc2VzICh3aXRoIG9wdGlvbmFsIGNjIC8gYmNjIGFuZCBmaWxlIGF0dGFjaG1lbnRzKSwgYSBzdWJqZWN0LFxuICBhbmQgYSBib2R5OyB0aGUgdXNlciBpcyBwcm9tcHRlZCB0byBhcHByb3ZlIHRoZSBzZW5kIGJlZm9yZSBpdCBnb2VzXG4gIG91dCAodGhpcyB0b29sIGlzIGFwcHJvdmFsLWdhdGVkKS4gVXNlIGl0IG9ubHkgd2hlbiB0aGUgdXNlclxuICBleHBsaWNpdGx5IGFza3MgdG8gc2VuZCAvIGZvcndhcmQgLyBzaGFyZSBzb21ldGhpbmcgdmlhIGVtYWlsIC1cbiAgbmV2ZXIgYXV0b25vbW91c2x5LiBLZWVwIHN1YmplY3RzIHNob3J0IGFuZCBib2RpZXMgc2VsZi1jb250YWluZWQ6XG4gIHRoZSByZWNpcGllbnQgaGFzIG5vbmUgb2YgdGhlIGNoYXQgY29udGV4dC4gV3JpdGUgdGhlIGJvZHkgaW5cbiAgR2l0SHViLUZsYXZvcmVkIE1hcmtkb3duIC0gaGVhZGluZ3MsIGxpc3RzLCBhbmQgcmVhbCBNYXJrZG93blxuICB0YWJsZXMgLSBub3QgQVNDSUkgYXJ0IChubyBcIj09PT09XCIgZGl2aWRlcnMgb3Igc3BhY2UvcGlwZS1kcmF3blxuICB0YWJsZXMpOyBpdCBpcyByZW5kZXJlZCB0byBIVE1MIGJlZm9yZSBzZW5kaW5nLlxuYCk7XG5cbi8qKiBPcHRpb25zIGFjY2VwdGVkIGJ5IHtAbGluayBlbWFpbFRvb2x9LiAqL1xuZXhwb3J0IGludGVyZmFjZSBFbWFpbFRvb2xPcHRpb25zIHtcbiAgLyoqXG4gICAqIE92ZXJyaWRlIHRoZSB0b29sIGlkLiBEZWZhdWx0cyB0byBgXCJzZW5kX2VtYWlsXCJgOyB0aGUgY2hhdCBVSSdzXG4gICAqIGFwcHJvdmFsIGdhdGUga2V5cyBvZmYgdGhpcyBpZCwgc28ga2VlcCBpdCB1bmxlc3MgeW91IGFsc28gdGVhY2hcbiAgICogdGhlIGNsaWVudCBhYm91dCB0aGUgbmV3IG5hbWUuXG4gICAqL1xuICBpZD86IHN0cmluZztcbn1cblxuLyoqXG4gKiBCdWlsZCB0aGUgYXBwcm92YWwtZ2F0ZWQgYHNlbmRfZW1haWxgIHRvb2wuIFNwcmVhZCBpdCBpbnRvIHRoZSBhZ2VudHNcbiAqIHRoYXQgc2hvdWxkIGJlIGFibGUgdG8gZHJhZnQgbWFpbDsgaXQgaXMgaW50ZW50aW9uYWxseSBub3QgaW5zdGFsbGVkXG4gKiBldmVyeXdoZXJlLlxuICpcbiAqIEBleGFtcGxlXG4gKiBgYGB0c1xuICogaW1wb3J0IHsgZW1haWxUb29sIH0gZnJvbSBcIkBkYngtdG9vbHMvZW1haWxcIjtcbiAqIGltcG9ydCB7IGNyZWF0ZUFnZW50IH0gZnJvbSBcIkBkYngtdG9vbHMvYXBwa2l0LW1hc3RyYVwiO1xuICpcbiAqIGNvbnN0IHN1cHBvcnQgPSBjcmVhdGVBZ2VudCh7XG4gKiAgIGluc3RydWN0aW9uczogXCIuLi5cIixcbiAqICAgdG9vbHM6ICgpID0+ICh7IHNlbmRfZW1haWw6IGVtYWlsVG9vbCgpIH0pLFxuICogfSk7XG4gKiBgYGBcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGVtYWlsVG9vbChvcHRzOiBFbWFpbFRvb2xPcHRpb25zID0ge30pIHtcbiAgcmV0dXJuIGNyZWF0ZVRvb2woe1xuICAgIGlkOiBvcHRzLmlkID8/IFwic2VuZF9lbWFpbFwiLFxuICAgIGRlc2NyaXB0aW9uOiBTRU5EX0VNQUlMX0RFU0NSSVBUSU9OLFxuICAgIGlucHV0U2NoZW1hOiBlbWFpbC5lbWFpbE1lc3NhZ2VTY2hlbWEsXG4gICAgb3V0cHV0U2NoZW1hOiBlbWFpbC5lbWFpbFJlc3VsdFNjaGVtYSxcbiAgICByZXF1aXJlQXBwcm92YWw6IHRydWUsXG4gICAgZXhlY3V0ZTogYXN5bmMgKGlucHV0LCBjb250ZXh0KSA9PiB7XG4gICAgICBjb25zdCBtZXNzYWdlID0gZW1haWwuZW1haWxNZXNzYWdlU2NoZW1hLnBhcnNlKGlucHV0KTtcbiAgICAgIGNvbnN0IHsgY29uZmlnIH0gPSBnZXRFbWFpbFJ1bnRpbWUoKTtcbiAgICAgIGNvbnN0IGN0eCA9IGdldEV4ZWN1dGlvbkNvbnRleHQoKTtcbiAgICAgIGNvbnN0IHVzZXJFbWFpbCA9IFwiaXNVc2VyQ29udGV4dFwiIGluIGN0eCA/IGN0eC51c2VyRW1haWwgOiB1bmRlZmluZWQ7XG4gICAgICBjb25zdCBmcm9tID0gcmVzb2x2ZVNlbmRlckFkZHJlc3MoY29uZmlnLCB1c2VyRW1haWwpO1xuICAgICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgc2VuZEVtYWlsKG1lc3NhZ2UsIGZyb20sIGNvbnRleHQ/LmFib3J0U2lnbmFsKTtcbiAgICAgIGxvZ2dlci5pbmZvKFwic2VudFwiLCB7XG4gICAgICAgIHRvOiByZXN1bHQucmVjaXBpZW50LFxuICAgICAgICBmcm9tOiByZXN1bHQuZnJvbSxcbiAgICAgICAgLi4uKHJlc3VsdC5tZXNzYWdlSWQgPyB7IG1lc3NhZ2VJZDogcmVzdWx0Lm1lc3NhZ2VJZCB9IDoge30pLFxuICAgICAgfSk7XG4gICAgICByZXR1cm4gcmVzdWx0O1xuICAgIH0sXG4gIH0pO1xufVxuIl19
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The email runtime: a lazily-built, process-wide dispatcher plus its
3
+ * resolved config, and {@link sendEmail} which sends one
4
+ * {@link EmailMessage} through it. In SMTP mode the runtime holds a
5
+ * memoized nodemailer transport (shared by the plugin's setup and the
6
+ * agent tool, so they reuse one connection pool); in file/outbox mode it
7
+ * holds no transport and {@link sendEmail} writes HTML to disk instead.
8
+ * The first caller (normally the plugin at setup) primes it with the
9
+ * plugin's config; later callers reuse it.
10
+ *
11
+ * The runtime also carries the {@link EmailExecutor} every outbound send runs
12
+ * through. The plugin installs its own `execute()` there at setup, which is
13
+ * how the Mastra tool - a plain function with no plugin instance in scope -
14
+ * still gets AppKit's retry / timeout / telemetry chain. Without a registered
15
+ * plugin (a direct call from a script or a test) the send still runs, just
16
+ * without interceptors.
17
+ *
18
+ * Every entry point takes an optional {@link AbortSignal} so the plugin's
19
+ * `execute()` timeout and a client disconnect both stop the caller waiting
20
+ * on SMTP.
21
+ *
22
+ * @module
23
+ */
24
+ import { type ExecutionResult } from "@databricks/appkit";
25
+ import type { EmailMessage, EmailResult } from "@dbx-tools/shared-email";
26
+ import { type Transporter } from "nodemailer";
27
+ import { type EmailPluginConfig, type ResolvedEmailConfig } from "./config.js";
28
+ import { type EmailExecutionSettings } from "./defaults.js";
29
+ /**
30
+ * Runs one outbound send through AppKit's interceptor chain. Matches
31
+ * `Plugin.execute()`, which never throws: a failure comes back as
32
+ * `{ ok: false }`.
33
+ */
34
+ export type EmailExecutor = <T>(fn: (signal?: AbortSignal) => Promise<T>, settings: EmailExecutionSettings) => Promise<ExecutionResult<T>>;
35
+ /** The shared dispatcher, its resolved config, and the send executor. */
36
+ export interface EmailRuntime {
37
+ /** Present only in SMTP mode. */
38
+ transporter?: Transporter;
39
+ config: ResolvedEmailConfig;
40
+ execute: EmailExecutor;
41
+ }
42
+ /**
43
+ * Return the shared runtime, building it on first use by resolving
44
+ * `overrides` over the environment (`SMTP_HOST`, `SMTP_PORT`,
45
+ * `SMTP_SECURE`, `SMTP_USER`, `SMTP_PASSWORD`, `EMAIL_DOMAIN`,
46
+ * `EMAIL_FROM`, `EMAIL_ALLOWED_SENDERS`, `EMAIL_SENDER_POLICY`,
47
+ * `EMAIL_OUTBOX_MODE`, `EMAIL_OUTBOX_DIR`) through
48
+ * {@link resolveEmailConfig}. With SMTP credentials present it holds a
49
+ * nodemailer transport and its connection pool; otherwise it is in
50
+ * file/outbox mode and holds none.
51
+ *
52
+ * `overrides` is read only on the call that builds the runtime, so prime it
53
+ * from the plugin's config at setup; later callers (the tool's `execute`,
54
+ * the sender-options route) pass nothing and get the same instance. Throws
55
+ * whatever {@link resolveEmailConfig} throws for an unusable configuration.
56
+ */
57
+ export declare function getEmailRuntime(overrides?: EmailPluginConfig): EmailRuntime;
58
+ /**
59
+ * Install the executor outbound sends run through. The plugin calls this at
60
+ * setup with its own `execute()`; a second call replaces the previous one, so
61
+ * a re-registered plugin does not leave the tools bound to a dead instance.
62
+ */
63
+ export declare function setEmailExecutor(execute: EmailExecutor): void;
64
+ /**
65
+ * Drop the memoized runtime, closing the SMTP connection pool, so the next
66
+ * {@link getEmailRuntime} rebuilds it from fresh config and stops calling
67
+ * through a torn-down plugin's `execute()`. Idempotent, and the plugin's
68
+ * `shutdown()` hook.
69
+ */
70
+ export declare function resetEmailRuntime(): void;
71
+ /**
72
+ * Run one non-idempotent write through the shared executor and unwrap it.
73
+ *
74
+ * `execute()` never throws, so a failed send arrives as `{ ok: false }` with a
75
+ * status the interceptors already sanitized; it is logged here and re-raised
76
+ * as a stable {@link ExecutionError} so an upstream message never becomes the
77
+ * caller's error text. `signal` is the caller's own cancellation (an agent
78
+ * run, a request teardown); it is merged with the signal the timeout
79
+ * interceptor supplies so either one unwinds the I/O.
80
+ */
81
+ export declare function executeWrite<T>(operation: string, settings: EmailExecutionSettings, fn: (signal?: AbortSignal) => Promise<T>, signal?: AbortSignal): Promise<T>;
82
+ /**
83
+ * Open and tear down one SMTP connection to prove the host, port, and
84
+ * credentials work. Called at plugin setup so a bad relay shows up in the
85
+ * boot logs rather than on the first approved send.
86
+ */
87
+ export declare function verifyEmailTransport(transporter: Transporter | undefined, signal?: AbortSignal): Promise<void>;
88
+ /**
89
+ * Send (SMTP mode) or persist (file/outbox mode) one message from the
90
+ * resolved `from` address. `to` (and optional `cc` / `bcc`) each accept
91
+ * one or more addresses, and `attachments` are forwarded as files. The
92
+ * body is markdown: SMTP sends it as both a plain-text part (the raw
93
+ * source) and an HTML part (rendered), and the outbox embeds the
94
+ * rendered HTML in a document. In file mode the returned `messageId` is
95
+ * the path written. Throws when `to` carries no recipient, when the body
96
+ * or attachments exceed the plugin's caps, or when `from` is not permitted
97
+ * by the effective sender allow-list.
98
+ *
99
+ * The recipient, cap, and sender checks run before the interceptor chain so
100
+ * their specific status and actionable message reach the caller instead of
101
+ * the chain's stable failure text. `signal` cancels the send.
102
+ */
103
+ export declare function sendEmail(message: EmailMessage, from: string, signal?: AbortSignal): Promise<EmailResult>;