@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.
- package/lib/index.d.ts +26 -0
- package/lib/index.js +24 -0
- package/lib/src/brand.d.ts +47 -0
- package/lib/src/brand.js +44 -0
- package/lib/src/config.d.ts +143 -0
- package/lib/src/config.js +212 -0
- package/lib/src/defaults.d.ts +60 -0
- package/lib/src/defaults.js +64 -0
- package/lib/src/email-html.d.ts +51 -0
- package/lib/src/email-html.js +146 -0
- package/lib/src/markdown.d.ts +22 -0
- package/lib/src/markdown.js +81 -0
- package/lib/src/outbox.d.ts +22 -0
- package/lib/src/outbox.js +60 -0
- package/lib/src/plugin.d.ts +165 -0
- package/lib/src/plugin.js +294 -0
- package/lib/src/sender.d.ts +71 -0
- package/lib/src/sender.js +152 -0
- package/lib/src/tool.d.ts +68 -0
- package/lib/src/tool.js +82 -0
- package/lib/src/transport.d.ts +103 -0
- package/lib/src/transport.js +303 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +11 -7
|
@@ -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>;
|
package/lib/src/tool.js
ADDED
|
@@ -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>;
|