@dbx-tools/email 0.3.44 → 0.4.1

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,64 @@
1
+ /**
2
+ * Interceptor defaults and hard payload caps for the email plugin.
3
+ *
4
+ * The execution settings are what the runtime's executor hands
5
+ * `Plugin.execute()`, kept here rather than at the call sites so the
6
+ * caching / retry / timeout posture of every outbound operation is
7
+ * reviewable in one place. The caps bound the one unbounded input the
8
+ * plugin accepts: a model-drafted message with attachments.
9
+ *
10
+ * @module
11
+ */
12
+ /** Ceiling on how long a single SMTP conversation may take. */
13
+ export const SEND_TIMEOUT_MS = 30_000;
14
+ /** Ceiling on the SMTP handshake performed at plugin setup. */
15
+ export const VERIFY_TIMEOUT_MS = 15_000;
16
+ /** Attempts allowed for the setup-time SMTP handshake, including the first. */
17
+ export const VERIFY_ATTEMPTS = 3;
18
+ /** Largest single attachment accepted, in decoded bytes (10 MiB). */
19
+ export const MAX_ATTACHMENT_BYTES = 10_485_760;
20
+ /** Largest combined attachment payload accepted, in decoded bytes (20 MiB). */
21
+ export const MAX_ATTACHMENTS_TOTAL_BYTES = 20_971_520;
22
+ /** Largest number of attachments accepted on one message. */
23
+ export const MAX_ATTACHMENT_COUNT = 20;
24
+ /** Largest markdown body accepted, in characters. */
25
+ export const MAX_BODY_CHARS = 200_000;
26
+ /** Execution settings for a send (SMTP dispatch or an outbox write). */
27
+ export const EMAIL_SEND_SETTINGS = {
28
+ default: {
29
+ // Cache disabled: a send is a side effect, not a value. Replaying a
30
+ // cached result would report success for a message never handed to SMTP.
31
+ cache: { enabled: false },
32
+ // Retry disabled: SMTP delivery is not idempotent. A `sendMail` that
33
+ // times out may already have queued the message, so a second attempt
34
+ // risks delivering the mail twice.
35
+ retry: { enabled: false },
36
+ timeout: SEND_TIMEOUT_MS,
37
+ },
38
+ };
39
+ /** Execution settings for the setup-time SMTP connectivity check. */
40
+ export const EMAIL_VERIFY_SETTINGS = {
41
+ default: {
42
+ // Cache disabled: connectivity is a point-in-time fact about the server,
43
+ // and this runs once per boot, so there is nothing to reuse.
44
+ cache: { enabled: false },
45
+ // Retry enabled: the handshake has no side effect, and a cold SMTP relay
46
+ // or a slow DNS answer at boot is exactly the transient failure that
47
+ // should not take the app down.
48
+ retry: { enabled: true, attempts: VERIFY_ATTEMPTS },
49
+ timeout: VERIFY_TIMEOUT_MS,
50
+ },
51
+ };
52
+ /** Execution settings for the sender-options lookup. */
53
+ export const EMAIL_SENDERS_SETTINGS = {
54
+ default: {
55
+ // Cache disabled: the options are computed from already-resolved config
56
+ // and the caller's own address, so a cache would add a cross-identity
57
+ // leak risk for no measurable saving.
58
+ cache: { enabled: false },
59
+ // Retry disabled: the lookup performs no I/O, so a failure is
60
+ // deterministic and a second attempt would fail identically.
61
+ retry: { enabled: false },
62
+ },
63
+ };
64
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVmYXVsdHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvZGVmYXVsdHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7R0FVRztBQXdCSCwrREFBK0Q7QUFDL0QsTUFBTSxDQUFDLE1BQU0sZUFBZSxHQUFHLE1BQU0sQ0FBQztBQUV0QywrREFBK0Q7QUFDL0QsTUFBTSxDQUFDLE1BQU0saUJBQWlCLEdBQUcsTUFBTSxDQUFDO0FBRXhDLCtFQUErRTtBQUMvRSxNQUFNLENBQUMsTUFBTSxlQUFlLEdBQUcsQ0FBQyxDQUFDO0FBRWpDLHFFQUFxRTtBQUNyRSxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBRyxVQUFVLENBQUM7QUFFL0MsK0VBQStFO0FBQy9FLE1BQU0sQ0FBQyxNQUFNLDJCQUEyQixHQUFHLFVBQVUsQ0FBQztBQUV0RCw2REFBNkQ7QUFDN0QsTUFBTSxDQUFDLE1BQU0sb0JBQW9CLEdBQUcsRUFBRSxDQUFDO0FBRXZDLHFEQUFxRDtBQUNyRCxNQUFNLENBQUMsTUFBTSxjQUFjLEdBQUcsT0FBTyxDQUFDO0FBRXRDLHdFQUF3RTtBQUN4RSxNQUFNLENBQUMsTUFBTSxtQkFBbUIsR0FBMkI7SUFDekQsT0FBTyxFQUFFO1FBQ1Asb0VBQW9FO1FBQ3BFLHlFQUF5RTtRQUN6RSxLQUFLLEVBQUUsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFO1FBQ3pCLHFFQUFxRTtRQUNyRSxxRUFBcUU7UUFDckUsbUNBQW1DO1FBQ25DLEtBQUssRUFBRSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUU7UUFDekIsT0FBTyxFQUFFLGVBQWU7S0FDekI7Q0FDRixDQUFDO0FBRUYscUVBQXFFO0FBQ3JFLE1BQU0sQ0FBQyxNQUFNLHFCQUFxQixHQUEyQjtJQUMzRCxPQUFPLEVBQUU7UUFDUCx5RUFBeUU7UUFDekUsNkRBQTZEO1FBQzdELEtBQUssRUFBRSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUU7UUFDekIseUVBQXlFO1FBQ3pFLHFFQUFxRTtRQUNyRSxnQ0FBZ0M7UUFDaEMsS0FBSyxFQUFFLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsZUFBZSxFQUFFO1FBQ25ELE9BQU8sRUFBRSxpQkFBaUI7S0FDM0I7Q0FDRixDQUFDO0FBRUYsd0RBQXdEO0FBQ3hELE1BQU0sQ0FBQyxNQUFNLHNCQUFzQixHQUEyQjtJQUM1RCxPQUFPLEVBQUU7UUFDUCx3RUFBd0U7UUFDeEUsc0VBQXNFO1FBQ3RFLHNDQUFzQztRQUN0QyxLQUFLLEVBQUUsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFO1FBQ3pCLDhEQUE4RDtRQUM5RCw2REFBNkQ7UUFDN0QsS0FBSyxFQUFFLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRTtLQUMxQjtDQUNGLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIEludGVyY2VwdG9yIGRlZmF1bHRzIGFuZCBoYXJkIHBheWxvYWQgY2FwcyBmb3IgdGhlIGVtYWlsIHBsdWdpbi5cbiAqXG4gKiBUaGUgZXhlY3V0aW9uIHNldHRpbmdzIGFyZSB3aGF0IHRoZSBydW50aW1lJ3MgZXhlY3V0b3IgaGFuZHNcbiAqIGBQbHVnaW4uZXhlY3V0ZSgpYCwga2VwdCBoZXJlIHJhdGhlciB0aGFuIGF0IHRoZSBjYWxsIHNpdGVzIHNvIHRoZVxuICogY2FjaGluZyAvIHJldHJ5IC8gdGltZW91dCBwb3N0dXJlIG9mIGV2ZXJ5IG91dGJvdW5kIG9wZXJhdGlvbiBpc1xuICogcmV2aWV3YWJsZSBpbiBvbmUgcGxhY2UuIFRoZSBjYXBzIGJvdW5kIHRoZSBvbmUgdW5ib3VuZGVkIGlucHV0IHRoZVxuICogcGx1Z2luIGFjY2VwdHM6IGEgbW9kZWwtZHJhZnRlZCBtZXNzYWdlIHdpdGggYXR0YWNobWVudHMuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbi8qKlxuICogVGhlIGBQbHVnaW5FeGVjdXRlQ29uZmlnYCBzbGljZSB0aGlzIHBhY2thZ2Ugc2V0cy4gTWlycm9yZWQgc3RydWN0dXJhbGx5XG4gKiBiZWNhdXNlIEFwcEtpdCdzIGBQbHVnaW5FeGVjdXRlQ29uZmlnYCBsaXZlcyBiZWhpbmQgYSBzdWJwYXRoIGl0cyBgZXhwb3J0c2BcbiAqIG1hcCBkb2VzIG5vdCBwdWJsaXNoLCBzbyB0aGUgbm9taW5hbCB0eXBlIGNhbm5vdCBiZSBpbXBvcnRlZC4gV3JpdHRlbiBhcyBhXG4gKiB0eXBlIGFsaWFzIHJhdGhlciB0aGFuIGFuIGludGVyZmFjZSBzbyBpdCBzdGF5cyBhc3NpZ25hYmxlIHRvIHRoZSBub21pbmFsXG4gKiB0eXBlJ3MgaW5kZXggc2lnbmF0dXJlLlxuICovXG5leHBvcnQgdHlwZSBFbWFpbEV4ZWN1dGVDb25maWcgPSB7XG4gIGNhY2hlPzogeyBlbmFibGVkPzogYm9vbGVhbjsgdHRsPzogbnVtYmVyOyBjYWNoZUtleT86IChzdHJpbmcgfCBudW1iZXIgfCBvYmplY3QpW10gfTtcbiAgcmV0cnk/OiB7IGVuYWJsZWQ/OiBib29sZWFuOyBhdHRlbXB0cz86IG51bWJlcjsgaW5pdGlhbERlbGF5PzogbnVtYmVyOyBtYXhEZWxheT86IG51bWJlciB9O1xuICB0aW1lb3V0PzogbnVtYmVyO1xufTtcblxuLyoqXG4gKiBUaGUgYFBsdWdpbkV4ZWN1dGlvblNldHRpbmdzYCBzaGFwZSBhY2NlcHRlZCBieSBBcHBLaXQncyBgUGx1Z2luLmV4ZWN1dGUoKWAuXG4gKiBNaXJyb3JlZCBzdHJ1Y3R1cmFsbHkgZm9yIHRoZSBzYW1lIHJlYXNvbiBhcyB7QGxpbmsgRW1haWxFeGVjdXRlQ29uZmlnfS5cbiAqL1xuZXhwb3J0IHR5cGUgRW1haWxFeGVjdXRpb25TZXR0aW5ncyA9IHtcbiAgZGVmYXVsdDogRW1haWxFeGVjdXRlQ29uZmlnO1xuICB1c2VyPzogRW1haWxFeGVjdXRlQ29uZmlnO1xufTtcblxuLyoqIENlaWxpbmcgb24gaG93IGxvbmcgYSBzaW5nbGUgU01UUCBjb252ZXJzYXRpb24gbWF5IHRha2UuICovXG5leHBvcnQgY29uc3QgU0VORF9USU1FT1VUX01TID0gMzBfMDAwO1xuXG4vKiogQ2VpbGluZyBvbiB0aGUgU01UUCBoYW5kc2hha2UgcGVyZm9ybWVkIGF0IHBsdWdpbiBzZXR1cC4gKi9cbmV4cG9ydCBjb25zdCBWRVJJRllfVElNRU9VVF9NUyA9IDE1XzAwMDtcblxuLyoqIEF0dGVtcHRzIGFsbG93ZWQgZm9yIHRoZSBzZXR1cC10aW1lIFNNVFAgaGFuZHNoYWtlLCBpbmNsdWRpbmcgdGhlIGZpcnN0LiAqL1xuZXhwb3J0IGNvbnN0IFZFUklGWV9BVFRFTVBUUyA9IDM7XG5cbi8qKiBMYXJnZXN0IHNpbmdsZSBhdHRhY2htZW50IGFjY2VwdGVkLCBpbiBkZWNvZGVkIGJ5dGVzICgxMCBNaUIpLiAqL1xuZXhwb3J0IGNvbnN0IE1BWF9BVFRBQ0hNRU5UX0JZVEVTID0gMTBfNDg1Xzc2MDtcblxuLyoqIExhcmdlc3QgY29tYmluZWQgYXR0YWNobWVudCBwYXlsb2FkIGFjY2VwdGVkLCBpbiBkZWNvZGVkIGJ5dGVzICgyMCBNaUIpLiAqL1xuZXhwb3J0IGNvbnN0IE1BWF9BVFRBQ0hNRU5UU19UT1RBTF9CWVRFUyA9IDIwXzk3MV81MjA7XG5cbi8qKiBMYXJnZXN0IG51bWJlciBvZiBhdHRhY2htZW50cyBhY2NlcHRlZCBvbiBvbmUgbWVzc2FnZS4gKi9cbmV4cG9ydCBjb25zdCBNQVhfQVRUQUNITUVOVF9DT1VOVCA9IDIwO1xuXG4vKiogTGFyZ2VzdCBtYXJrZG93biBib2R5IGFjY2VwdGVkLCBpbiBjaGFyYWN0ZXJzLiAqL1xuZXhwb3J0IGNvbnN0IE1BWF9CT0RZX0NIQVJTID0gMjAwXzAwMDtcblxuLyoqIEV4ZWN1dGlvbiBzZXR0aW5ncyBmb3IgYSBzZW5kIChTTVRQIGRpc3BhdGNoIG9yIGFuIG91dGJveCB3cml0ZSkuICovXG5leHBvcnQgY29uc3QgRU1BSUxfU0VORF9TRVRUSU5HUzogRW1haWxFeGVjdXRpb25TZXR0aW5ncyA9IHtcbiAgZGVmYXVsdDoge1xuICAgIC8vIENhY2hlIGRpc2FibGVkOiBhIHNlbmQgaXMgYSBzaWRlIGVmZmVjdCwgbm90IGEgdmFsdWUuIFJlcGxheWluZyBhXG4gICAgLy8gY2FjaGVkIHJlc3VsdCB3b3VsZCByZXBvcnQgc3VjY2VzcyBmb3IgYSBtZXNzYWdlIG5ldmVyIGhhbmRlZCB0byBTTVRQLlxuICAgIGNhY2hlOiB7IGVuYWJsZWQ6IGZhbHNlIH0sXG4gICAgLy8gUmV0cnkgZGlzYWJsZWQ6IFNNVFAgZGVsaXZlcnkgaXMgbm90IGlkZW1wb3RlbnQuIEEgYHNlbmRNYWlsYCB0aGF0XG4gICAgLy8gdGltZXMgb3V0IG1heSBhbHJlYWR5IGhhdmUgcXVldWVkIHRoZSBtZXNzYWdlLCBzbyBhIHNlY29uZCBhdHRlbXB0XG4gICAgLy8gcmlza3MgZGVsaXZlcmluZyB0aGUgbWFpbCB0d2ljZS5cbiAgICByZXRyeTogeyBlbmFibGVkOiBmYWxzZSB9LFxuICAgIHRpbWVvdXQ6IFNFTkRfVElNRU9VVF9NUyxcbiAgfSxcbn07XG5cbi8qKiBFeGVjdXRpb24gc2V0dGluZ3MgZm9yIHRoZSBzZXR1cC10aW1lIFNNVFAgY29ubmVjdGl2aXR5IGNoZWNrLiAqL1xuZXhwb3J0IGNvbnN0IEVNQUlMX1ZFUklGWV9TRVRUSU5HUzogRW1haWxFeGVjdXRpb25TZXR0aW5ncyA9IHtcbiAgZGVmYXVsdDoge1xuICAgIC8vIENhY2hlIGRpc2FibGVkOiBjb25uZWN0aXZpdHkgaXMgYSBwb2ludC1pbi10aW1lIGZhY3QgYWJvdXQgdGhlIHNlcnZlcixcbiAgICAvLyBhbmQgdGhpcyBydW5zIG9uY2UgcGVyIGJvb3QsIHNvIHRoZXJlIGlzIG5vdGhpbmcgdG8gcmV1c2UuXG4gICAgY2FjaGU6IHsgZW5hYmxlZDogZmFsc2UgfSxcbiAgICAvLyBSZXRyeSBlbmFibGVkOiB0aGUgaGFuZHNoYWtlIGhhcyBubyBzaWRlIGVmZmVjdCwgYW5kIGEgY29sZCBTTVRQIHJlbGF5XG4gICAgLy8gb3IgYSBzbG93IEROUyBhbnN3ZXIgYXQgYm9vdCBpcyBleGFjdGx5IHRoZSB0cmFuc2llbnQgZmFpbHVyZSB0aGF0XG4gICAgLy8gc2hvdWxkIG5vdCB0YWtlIHRoZSBhcHAgZG93bi5cbiAgICByZXRyeTogeyBlbmFibGVkOiB0cnVlLCBhdHRlbXB0czogVkVSSUZZX0FUVEVNUFRTIH0sXG4gICAgdGltZW91dDogVkVSSUZZX1RJTUVPVVRfTVMsXG4gIH0sXG59O1xuXG4vKiogRXhlY3V0aW9uIHNldHRpbmdzIGZvciB0aGUgc2VuZGVyLW9wdGlvbnMgbG9va3VwLiAqL1xuZXhwb3J0IGNvbnN0IEVNQUlMX1NFTkRFUlNfU0VUVElOR1M6IEVtYWlsRXhlY3V0aW9uU2V0dGluZ3MgPSB7XG4gIGRlZmF1bHQ6IHtcbiAgICAvLyBDYWNoZSBkaXNhYmxlZDogdGhlIG9wdGlvbnMgYXJlIGNvbXB1dGVkIGZyb20gYWxyZWFkeS1yZXNvbHZlZCBjb25maWdcbiAgICAvLyBhbmQgdGhlIGNhbGxlcidzIG93biBhZGRyZXNzLCBzbyBhIGNhY2hlIHdvdWxkIGFkZCBhIGNyb3NzLWlkZW50aXR5XG4gICAgLy8gbGVhayByaXNrIGZvciBubyBtZWFzdXJhYmxlIHNhdmluZy5cbiAgICBjYWNoZTogeyBlbmFibGVkOiBmYWxzZSB9LFxuICAgIC8vIFJldHJ5IGRpc2FibGVkOiB0aGUgbG9va3VwIHBlcmZvcm1zIG5vIEkvTywgc28gYSBmYWlsdXJlIGlzXG4gICAgLy8gZGV0ZXJtaW5pc3RpYyBhbmQgYSBzZWNvbmQgYXR0ZW1wdCB3b3VsZCBmYWlsIGlkZW50aWNhbGx5LlxuICAgIHJldHJ5OiB7IGVuYWJsZWQ6IGZhbHNlIH0sXG4gIH0sXG59O1xuIl19
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Email HTML assembly: render a markdown body into a branded, responsive
3
+ * email layout and inline the stylesheet with `juice`.
4
+ *
5
+ * The layout is the classic email-safe pattern (a centered 600px
6
+ * table-based container with a header band, content card, and optional
7
+ * footer) - the same shape MJML emits, hand-built here because MJML's
8
+ * toolchain pulls fast-moving browser-data deps (caniuse-lite,
9
+ * baseline-browser-mapping) that are awkward to install behind a
10
+ * locked-down registry. Inlining matters because real clients (Gmail,
11
+ * Outlook) strip `<style>` blocks and ignore class selectors; the same
12
+ * renderer feeds both the local outbox preview and the SMTP HTML part,
13
+ * so a browser and an inbox show the same thing.
14
+ *
15
+ * Brand styling (accent color, font, header logo) is optional: pass an
16
+ * {@link EmailBrand} to color the layout, or omit it for the neutral
17
+ * default. Branding is inlined here because the browser UI's `[data-brand]`
18
+ * CSS bridge can't reach an inbox (see `./brand`).
19
+ *
20
+ * @module
21
+ */
22
+ import { string } from "@dbx-tools/shared-core";
23
+ import type { EmailBrand } from "./brand.js";
24
+ /** Escape HTML-significant characters (re-exported from `@dbx-tools/shared-core`). */
25
+ export declare const escapeHtml: typeof string.escapeHtml;
26
+ /** Options for {@link renderEmailHtml}. */
27
+ export interface EmailHtmlOptions {
28
+ /** Markdown body. Rendered to HTML, then wrapped in the layout. */
29
+ body: string;
30
+ /** Header-band title and document `<title>`. Defaults to "Message". */
31
+ subject?: string;
32
+ /**
33
+ * Optional `[label, value]` envelope rows shown above the body (used
34
+ * by the outbox preview; omitted for SMTP sends, where the mail client
35
+ * shows the envelope itself).
36
+ */
37
+ headers?: ReadonlyArray<readonly [string, string]>;
38
+ /** Optional small-print footer line. Omitted when unset. */
39
+ footer?: string;
40
+ /**
41
+ * Optional brand styling for the layout (accent, font, header logo).
42
+ * Omit for the neutral default palette.
43
+ */
44
+ brand?: EmailBrand;
45
+ }
46
+ /**
47
+ * Render `body` (markdown) into a complete, style-inlined email document
48
+ * using the responsive layout. When `opts.brand` is set its accent, font,
49
+ * and logo style the layout; otherwise a neutral default palette is used.
50
+ */
51
+ export declare function renderEmailHtml(opts: EmailHtmlOptions): string;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Email HTML assembly: render a markdown body into a branded, responsive
3
+ * email layout and inline the stylesheet with `juice`.
4
+ *
5
+ * The layout is the classic email-safe pattern (a centered 600px
6
+ * table-based container with a header band, content card, and optional
7
+ * footer) - the same shape MJML emits, hand-built here because MJML's
8
+ * toolchain pulls fast-moving browser-data deps (caniuse-lite,
9
+ * baseline-browser-mapping) that are awkward to install behind a
10
+ * locked-down registry. Inlining matters because real clients (Gmail,
11
+ * Outlook) strip `<style>` blocks and ignore class selectors; the same
12
+ * renderer feeds both the local outbox preview and the SMTP HTML part,
13
+ * so a browser and an inbox show the same thing.
14
+ *
15
+ * Brand styling (accent color, font, header logo) is optional: pass an
16
+ * {@link EmailBrand} to color the layout, or omit it for the neutral
17
+ * default. Branding is inlined here because the browser UI's `[data-brand]`
18
+ * CSS bridge can't reach an inbox (see `./brand`).
19
+ *
20
+ * @module
21
+ */
22
+ import { string } from "@dbx-tools/shared-core";
23
+ import juice from "juice";
24
+ import { markdownToHtml } from "./markdown.js";
25
+ /**
26
+ * Rendered height of the header logo. Fixed rather than intrinsic because
27
+ * mail clients ignore CSS sizing on an image without an `height` attribute.
28
+ */
29
+ const LOGO_HEIGHT_PX = 28;
30
+ /** Neutral fallback styling when no brand is supplied. */
31
+ const DEFAULT_BRAND = {
32
+ accent: "#0b6bcb",
33
+ onAccent: "#ffffff",
34
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
35
+ };
36
+ /** Escape HTML-significant characters (re-exported from `@dbx-tools/shared-core`). */
37
+ export const escapeHtml = string.escapeHtml;
38
+ /**
39
+ * Content stylesheet inlined onto the markdown body (juice maps these
40
+ * onto elements). Outer layout styling is written inline directly so it
41
+ * survives even if inlining is skipped; the `@media` rule is preserved
42
+ * by juice for clients that honor it. Parameterized by the resolved accent
43
+ * so body links match the brand.
44
+ */
45
+ function contentCss(accent) {
46
+ return `
47
+ .email-body { font-size: 15px; line-height: 1.55; color: #1a1a1a; }
48
+ .email-body p { margin: 0 0 1rem; }
49
+ .email-body a { color: ${accent}; }
50
+ .email-body h1, .email-body h2, .email-body h3 { margin: 1.4rem 0 0.6rem; line-height: 1.25; }
51
+ .email-body ul, .email-body ol { margin: 0 0 1rem; padding-left: 1.4rem; }
52
+ .email-body table { border-collapse: collapse; margin: 1rem 0; width: 100%; font-size: 14px; }
53
+ .email-body th, .email-body td { border: 1px solid #d0d7de; padding: 6px 10px; text-align: left; }
54
+ .email-body th { background: #f6f8fa; font-weight: 600; }
55
+ .email-body code { background: #f1f3f5; padding: 2px 5px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; }
56
+ .email-body pre { background: #f1f3f5; padding: 12px; border-radius: 6px; overflow-x: auto; }
57
+ .email-body pre code { background: none; padding: 0; }
58
+ .email-body blockquote { margin: 1rem 0; padding: 0 1rem; color: #57606a; border-left: 3px solid #d0d7de; }
59
+ .email-body img { max-width: 100%; height: auto; }
60
+ .meta { border-collapse: collapse; font-size: 13px; margin-bottom: 4px; }
61
+ .meta th { text-align: left; padding: 2px 12px 2px 0; vertical-align: top; color: #6b7280; font-weight: 600; white-space: nowrap; }
62
+ .meta td { padding: 2px 0; color: #374151; }
63
+ @media only screen and (max-width: 620px) {
64
+ .container { width: 100% !important; }
65
+ .gutter { padding-left: 20px !important; padding-right: 20px !important; }
66
+ }`;
67
+ }
68
+ /** Render the optional envelope-header table block. */
69
+ function metaBlock(headers) {
70
+ if (!headers || headers.length === 0)
71
+ return "";
72
+ const rows = headers
73
+ .map(([label, value]) => `<tr><th>${escapeHtml(label)}</th><td>${escapeHtml(value)}</td></tr>`)
74
+ .join("");
75
+ return `<table role="presentation" class="meta"><tbody>${rows}</tbody></table>`;
76
+ }
77
+ /** Render the optional footer row. */
78
+ function footerRow(footer) {
79
+ if (!footer)
80
+ return "";
81
+ return `
82
+ <tr>
83
+ <td class="gutter" style="padding: 16px 32px; border-top: 1px solid #eaecef; color: #9aa0a6; font-size: 12px; line-height: 1.5;">
84
+ ${escapeHtml(footer)}
85
+ </td>
86
+ </tr>`;
87
+ }
88
+ /**
89
+ * Render the header-band content: the brand logo (when the brand supplies a
90
+ * renderable image) above the title, or just the title. The logo is capped
91
+ * at {@link LOGO_HEIGHT_PX} and tinted implicitly by its own artwork; the
92
+ * title always shows so the band is never empty.
93
+ */
94
+ function headerBand(title, brand, onAccent) {
95
+ const logo = brand.logoUrl
96
+ ? `<img src="${escapeHtml(brand.logoUrl)}" alt="${escapeHtml(brand.name ?? title)}" height="${LOGO_HEIGHT_PX}" style="height: ${LOGO_HEIGHT_PX}px; width: auto; display: block; margin-bottom: 8px;" />`
97
+ : "";
98
+ return `${logo}<span style="color: ${onAccent}; font-size: 18px; font-weight: 700; line-height: 1.3;">${escapeHtml(title)}</span>`;
99
+ }
100
+ /**
101
+ * Render `body` (markdown) into a complete, style-inlined email document
102
+ * using the responsive layout. When `opts.brand` is set its accent, font,
103
+ * and logo style the layout; otherwise a neutral default palette is used.
104
+ */
105
+ export function renderEmailHtml(opts) {
106
+ const title = opts.subject?.trim() || "Message";
107
+ const accent = opts.brand?.accent ?? DEFAULT_BRAND.accent;
108
+ const onAccent = opts.brand?.onAccent ?? DEFAULT_BRAND.onAccent;
109
+ const fontFamily = opts.brand?.fontFamily ?? DEFAULT_BRAND.fontFamily;
110
+ const brand = opts.brand ?? { accent, onAccent, fontFamily };
111
+ const doc = `<!doctype html>
112
+ <html lang="en">
113
+ <head>
114
+ <meta charset="utf-8" />
115
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
116
+ <meta name="color-scheme" content="light only" />
117
+ <title>${escapeHtml(title)}</title>
118
+ <style>${contentCss(accent)}
119
+ </style>
120
+ </head>
121
+ <body style="margin: 0; padding: 0; background-color: #f4f5f7; -webkit-text-size-adjust: 100%;">
122
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f5f7;">
123
+ <tr>
124
+ <td align="center" style="padding: 24px 12px;">
125
+ <table role="presentation" class="container" width="600" cellpadding="0" cellspacing="0" style="width: 600px; max-width: 100%; background-color: #ffffff; border-radius: 10px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); font-family: ${fontFamily};">
126
+ <tr>
127
+ <td class="gutter" style="padding: 20px 32px; background-color: ${accent};">
128
+ ${headerBand(title, brand, onAccent)}
129
+ </td>
130
+ </tr>
131
+ <tr>
132
+ <td class="gutter" style="padding: 24px 32px 8px;">
133
+ ${metaBlock(opts.headers)}
134
+ <div class="email-body">${markdownToHtml(opts.body)}</div>
135
+ </td>
136
+ </tr>${footerRow(opts.footer)}
137
+ </table>
138
+ </td>
139
+ </tr>
140
+ </table>
141
+ </body>
142
+ </html>
143
+ `;
144
+ return juice(doc);
145
+ }
146
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW1haWwtaHRtbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9lbWFpbC1odG1sLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQW9CRztBQUVILE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUNoRCxPQUFPLEtBQUssTUFBTSxPQUFPLENBQUM7QUFFMUIsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLFlBQVksQ0FBQztBQUU1Qzs7O0dBR0c7QUFDSCxNQUFNLGNBQWMsR0FBRyxFQUFFLENBQUM7QUFFMUIsMERBQTBEO0FBQzFELE1BQU0sYUFBYSxHQUFxRTtJQUN0RixNQUFNLEVBQUUsU0FBUztJQUNqQixRQUFRLEVBQUUsU0FBUztJQUNuQixVQUFVLEVBQUUsNkVBQTZFO0NBQzFGLENBQUM7QUFFRixzRkFBc0Y7QUFDdEYsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHLE1BQU0sQ0FBQyxVQUFVLENBQUM7QUFFNUM7Ozs7OztHQU1HO0FBQ0gsU0FBUyxVQUFVLENBQUMsTUFBYztJQUNoQyxPQUFPOzs7NkJBR29CLE1BQU07Ozs7Ozs7Ozs7Ozs7Ozs7O01BaUI3QixDQUFDO0FBQ1AsQ0FBQztBQXVCRCx1REFBdUQ7QUFDdkQsU0FBUyxTQUFTLENBQUMsT0FBb0M7SUFDckQsSUFBSSxDQUFDLE9BQU8sSUFBSSxPQUFPLENBQUMsTUFBTSxLQUFLLENBQUM7UUFBRSxPQUFPLEVBQUUsQ0FBQztJQUNoRCxNQUFNLElBQUksR0FBRyxPQUFPO1NBQ2pCLEdBQUcsQ0FBQyxDQUFDLENBQUMsS0FBSyxFQUFFLEtBQUssQ0FBQyxFQUFFLEVBQUUsQ0FBQyxXQUFXLFVBQVUsQ0FBQyxLQUFLLENBQUMsWUFBWSxVQUFVLENBQUMsS0FBSyxDQUFDLFlBQVksQ0FBQztTQUM5RixJQUFJLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDWixPQUFPLGtEQUFrRCxJQUFJLGtCQUFrQixDQUFDO0FBQ2xGLENBQUM7QUFFRCxzQ0FBc0M7QUFDdEMsU0FBUyxTQUFTLENBQUMsTUFBMEI7SUFDM0MsSUFBSSxDQUFDLE1BQU07UUFBRSxPQUFPLEVBQUUsQ0FBQztJQUN2QixPQUFPOzs7Z0JBR08sVUFBVSxDQUFDLE1BQU0sQ0FBQzs7Z0JBRWxCLENBQUM7QUFDakIsQ0FBQztBQUVEOzs7OztHQUtHO0FBQ0gsU0FBUyxVQUFVLENBQUMsS0FBYSxFQUFFLEtBQWlCLEVBQUUsUUFBZ0I7SUFDcEUsTUFBTSxJQUFJLEdBQUcsS0FBSyxDQUFDLE9BQU87UUFDeEIsQ0FBQyxDQUFDLGFBQWEsVUFBVSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsVUFBVSxVQUFVLENBQUMsS0FBSyxDQUFDLElBQUksSUFBSSxLQUFLLENBQUMsYUFBYSxjQUFjLG9CQUFvQixjQUFjLDBEQUEwRDtRQUN4TSxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQ1AsT0FBTyxHQUFHLElBQUksdUJBQXVCLFFBQVEsMkRBQTJELFVBQVUsQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDO0FBQ3JJLENBQUM7QUFFRDs7OztHQUlHO0FBQ0gsTUFBTSxVQUFVLGVBQWUsQ0FBQyxJQUFzQjtJQUNwRCxNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsT0FBTyxFQUFFLElBQUksRUFBRSxJQUFJLFNBQVMsQ0FBQztJQUNoRCxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsS0FBSyxFQUFFLE1BQU0sSUFBSSxhQUFhLENBQUMsTUFBTSxDQUFDO0lBQzFELE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxLQUFLLEVBQUUsUUFBUSxJQUFJLGFBQWEsQ0FBQyxRQUFRLENBQUM7SUFDaEUsTUFBTSxVQUFVLEdBQUcsSUFBSSxDQUFDLEtBQUssRUFBRSxVQUFVLElBQUksYUFBYSxDQUFDLFVBQVUsQ0FBQztJQUN0RSxNQUFNLEtBQUssR0FBZSxJQUFJLENBQUMsS0FBSyxJQUFJLEVBQUUsTUFBTSxFQUFFLFFBQVEsRUFBRSxVQUFVLEVBQUUsQ0FBQztJQUN6RSxNQUFNLEdBQUcsR0FBRzs7Ozs7O2FBTUQsVUFBVSxDQUFDLEtBQUssQ0FBQzthQUNqQixVQUFVLENBQUMsTUFBTSxDQUFDOzs7Ozs7O2tRQU9tTyxVQUFVOztnRkFFNUwsTUFBTTtrQkFDcEUsVUFBVSxDQUFDLEtBQUssRUFBRSxLQUFLLEVBQUUsUUFBUSxDQUFDOzs7OztrQkFLbEMsU0FBUyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUM7MENBQ0MsY0FBYyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7O21CQUVoRCxTQUFTLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQzs7Ozs7OztDQU94QyxDQUFDO0lBQ0EsT0FBTyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7QUFDcEIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogRW1haWwgSFRNTCBhc3NlbWJseTogcmVuZGVyIGEgbWFya2Rvd24gYm9keSBpbnRvIGEgYnJhbmRlZCwgcmVzcG9uc2l2ZVxuICogZW1haWwgbGF5b3V0IGFuZCBpbmxpbmUgdGhlIHN0eWxlc2hlZXQgd2l0aCBganVpY2VgLlxuICpcbiAqIFRoZSBsYXlvdXQgaXMgdGhlIGNsYXNzaWMgZW1haWwtc2FmZSBwYXR0ZXJuIChhIGNlbnRlcmVkIDYwMHB4XG4gKiB0YWJsZS1iYXNlZCBjb250YWluZXIgd2l0aCBhIGhlYWRlciBiYW5kLCBjb250ZW50IGNhcmQsIGFuZCBvcHRpb25hbFxuICogZm9vdGVyKSAtIHRoZSBzYW1lIHNoYXBlIE1KTUwgZW1pdHMsIGhhbmQtYnVpbHQgaGVyZSBiZWNhdXNlIE1KTUwnc1xuICogdG9vbGNoYWluIHB1bGxzIGZhc3QtbW92aW5nIGJyb3dzZXItZGF0YSBkZXBzIChjYW5pdXNlLWxpdGUsXG4gKiBiYXNlbGluZS1icm93c2VyLW1hcHBpbmcpIHRoYXQgYXJlIGF3a3dhcmQgdG8gaW5zdGFsbCBiZWhpbmQgYVxuICogbG9ja2VkLWRvd24gcmVnaXN0cnkuIElubGluaW5nIG1hdHRlcnMgYmVjYXVzZSByZWFsIGNsaWVudHMgKEdtYWlsLFxuICogT3V0bG9vaykgc3RyaXAgYDxzdHlsZT5gIGJsb2NrcyBhbmQgaWdub3JlIGNsYXNzIHNlbGVjdG9yczsgdGhlIHNhbWVcbiAqIHJlbmRlcmVyIGZlZWRzIGJvdGggdGhlIGxvY2FsIG91dGJveCBwcmV2aWV3IGFuZCB0aGUgU01UUCBIVE1MIHBhcnQsXG4gKiBzbyBhIGJyb3dzZXIgYW5kIGFuIGluYm94IHNob3cgdGhlIHNhbWUgdGhpbmcuXG4gKlxuICogQnJhbmQgc3R5bGluZyAoYWNjZW50IGNvbG9yLCBmb250LCBoZWFkZXIgbG9nbykgaXMgb3B0aW9uYWw6IHBhc3MgYW5cbiAqIHtAbGluayBFbWFpbEJyYW5kfSB0byBjb2xvciB0aGUgbGF5b3V0LCBvciBvbWl0IGl0IGZvciB0aGUgbmV1dHJhbFxuICogZGVmYXVsdC4gQnJhbmRpbmcgaXMgaW5saW5lZCBoZXJlIGJlY2F1c2UgdGhlIGJyb3dzZXIgVUkncyBgW2RhdGEtYnJhbmRdYFxuICogQ1NTIGJyaWRnZSBjYW4ndCByZWFjaCBhbiBpbmJveCAoc2VlIGAuL2JyYW5kYCkuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbmltcG9ydCB7IHN0cmluZyB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlXCI7XG5pbXBvcnQganVpY2UgZnJvbSBcImp1aWNlXCI7XG5pbXBvcnQgdHlwZSB7IEVtYWlsQnJhbmQgfSBmcm9tIFwiLi9icmFuZFwiO1xuaW1wb3J0IHsgbWFya2Rvd25Ub0h0bWwgfSBmcm9tIFwiLi9tYXJrZG93blwiO1xuXG4vKipcbiAqIFJlbmRlcmVkIGhlaWdodCBvZiB0aGUgaGVhZGVyIGxvZ28uIEZpeGVkIHJhdGhlciB0aGFuIGludHJpbnNpYyBiZWNhdXNlXG4gKiBtYWlsIGNsaWVudHMgaWdub3JlIENTUyBzaXppbmcgb24gYW4gaW1hZ2Ugd2l0aG91dCBhbiBgaGVpZ2h0YCBhdHRyaWJ1dGUuXG4gKi9cbmNvbnN0IExPR09fSEVJR0hUX1BYID0gMjg7XG5cbi8qKiBOZXV0cmFsIGZhbGxiYWNrIHN0eWxpbmcgd2hlbiBubyBicmFuZCBpcyBzdXBwbGllZC4gKi9cbmNvbnN0IERFRkFVTFRfQlJBTkQ6IFJlcXVpcmVkPFBpY2s8RW1haWxCcmFuZCwgXCJhY2NlbnRcIiB8IFwib25BY2NlbnRcIiB8IFwiZm9udEZhbWlseVwiPj4gPSB7XG4gIGFjY2VudDogXCIjMGI2YmNiXCIsXG4gIG9uQWNjZW50OiBcIiNmZmZmZmZcIixcbiAgZm9udEZhbWlseTogXCItYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICdTZWdvZSBVSScsIEhlbHZldGljYSwgQXJpYWwsIHNhbnMtc2VyaWZcIixcbn07XG5cbi8qKiBFc2NhcGUgSFRNTC1zaWduaWZpY2FudCBjaGFyYWN0ZXJzIChyZS1leHBvcnRlZCBmcm9tIGBAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlYCkuICovXG5leHBvcnQgY29uc3QgZXNjYXBlSHRtbCA9IHN0cmluZy5lc2NhcGVIdG1sO1xuXG4vKipcbiAqIENvbnRlbnQgc3R5bGVzaGVldCBpbmxpbmVkIG9udG8gdGhlIG1hcmtkb3duIGJvZHkgKGp1aWNlIG1hcHMgdGhlc2VcbiAqIG9udG8gZWxlbWVudHMpLiBPdXRlciBsYXlvdXQgc3R5bGluZyBpcyB3cml0dGVuIGlubGluZSBkaXJlY3RseSBzbyBpdFxuICogc3Vydml2ZXMgZXZlbiBpZiBpbmxpbmluZyBpcyBza2lwcGVkOyB0aGUgYEBtZWRpYWAgcnVsZSBpcyBwcmVzZXJ2ZWRcbiAqIGJ5IGp1aWNlIGZvciBjbGllbnRzIHRoYXQgaG9ub3IgaXQuIFBhcmFtZXRlcml6ZWQgYnkgdGhlIHJlc29sdmVkIGFjY2VudFxuICogc28gYm9keSBsaW5rcyBtYXRjaCB0aGUgYnJhbmQuXG4gKi9cbmZ1bmN0aW9uIGNvbnRlbnRDc3MoYWNjZW50OiBzdHJpbmcpOiBzdHJpbmcge1xuICByZXR1cm4gYFxuICAgIC5lbWFpbC1ib2R5IHsgZm9udC1zaXplOiAxNXB4OyBsaW5lLWhlaWdodDogMS41NTsgY29sb3I6ICMxYTFhMWE7IH1cbiAgICAuZW1haWwtYm9keSBwIHsgbWFyZ2luOiAwIDAgMXJlbTsgfVxuICAgIC5lbWFpbC1ib2R5IGEgeyBjb2xvcjogJHthY2NlbnR9OyB9XG4gICAgLmVtYWlsLWJvZHkgaDEsIC5lbWFpbC1ib2R5IGgyLCAuZW1haWwtYm9keSBoMyB7IG1hcmdpbjogMS40cmVtIDAgMC42cmVtOyBsaW5lLWhlaWdodDogMS4yNTsgfVxuICAgIC5lbWFpbC1ib2R5IHVsLCAuZW1haWwtYm9keSBvbCB7IG1hcmdpbjogMCAwIDFyZW07IHBhZGRpbmctbGVmdDogMS40cmVtOyB9XG4gICAgLmVtYWlsLWJvZHkgdGFibGUgeyBib3JkZXItY29sbGFwc2U6IGNvbGxhcHNlOyBtYXJnaW46IDFyZW0gMDsgd2lkdGg6IDEwMCU7IGZvbnQtc2l6ZTogMTRweDsgfVxuICAgIC5lbWFpbC1ib2R5IHRoLCAuZW1haWwtYm9keSB0ZCB7IGJvcmRlcjogMXB4IHNvbGlkICNkMGQ3ZGU7IHBhZGRpbmc6IDZweCAxMHB4OyB0ZXh0LWFsaWduOiBsZWZ0OyB9XG4gICAgLmVtYWlsLWJvZHkgdGggeyBiYWNrZ3JvdW5kOiAjZjZmOGZhOyBmb250LXdlaWdodDogNjAwOyB9XG4gICAgLmVtYWlsLWJvZHkgY29kZSB7IGJhY2tncm91bmQ6ICNmMWYzZjU7IHBhZGRpbmc6IDJweCA1cHg7IGJvcmRlci1yYWRpdXM6IDRweDsgZm9udC1mYW1pbHk6IHVpLW1vbm9zcGFjZSwgU0ZNb25vLVJlZ3VsYXIsIE1lbmxvLCBtb25vc3BhY2U7IGZvbnQtc2l6ZTogMC45ZW07IH1cbiAgICAuZW1haWwtYm9keSBwcmUgeyBiYWNrZ3JvdW5kOiAjZjFmM2Y1OyBwYWRkaW5nOiAxMnB4OyBib3JkZXItcmFkaXVzOiA2cHg7IG92ZXJmbG93LXg6IGF1dG87IH1cbiAgICAuZW1haWwtYm9keSBwcmUgY29kZSB7IGJhY2tncm91bmQ6IG5vbmU7IHBhZGRpbmc6IDA7IH1cbiAgICAuZW1haWwtYm9keSBibG9ja3F1b3RlIHsgbWFyZ2luOiAxcmVtIDA7IHBhZGRpbmc6IDAgMXJlbTsgY29sb3I6ICM1NzYwNmE7IGJvcmRlci1sZWZ0OiAzcHggc29saWQgI2QwZDdkZTsgfVxuICAgIC5lbWFpbC1ib2R5IGltZyB7IG1heC13aWR0aDogMTAwJTsgaGVpZ2h0OiBhdXRvOyB9XG4gICAgLm1ldGEgeyBib3JkZXItY29sbGFwc2U6IGNvbGxhcHNlOyBmb250LXNpemU6IDEzcHg7IG1hcmdpbi1ib3R0b206IDRweDsgfVxuICAgIC5tZXRhIHRoIHsgdGV4dC1hbGlnbjogbGVmdDsgcGFkZGluZzogMnB4IDEycHggMnB4IDA7IHZlcnRpY2FsLWFsaWduOiB0b3A7IGNvbG9yOiAjNmI3MjgwOyBmb250LXdlaWdodDogNjAwOyB3aGl0ZS1zcGFjZTogbm93cmFwOyB9XG4gICAgLm1ldGEgdGQgeyBwYWRkaW5nOiAycHggMDsgY29sb3I6ICMzNzQxNTE7IH1cbiAgICBAbWVkaWEgb25seSBzY3JlZW4gYW5kIChtYXgtd2lkdGg6IDYyMHB4KSB7XG4gICAgICAuY29udGFpbmVyIHsgd2lkdGg6IDEwMCUgIWltcG9ydGFudDsgfVxuICAgICAgLmd1dHRlciB7IHBhZGRpbmctbGVmdDogMjBweCAhaW1wb3J0YW50OyBwYWRkaW5nLXJpZ2h0OiAyMHB4ICFpbXBvcnRhbnQ7IH1cbiAgICB9YDtcbn1cblxuLyoqIE9wdGlvbnMgZm9yIHtAbGluayByZW5kZXJFbWFpbEh0bWx9LiAqL1xuZXhwb3J0IGludGVyZmFjZSBFbWFpbEh0bWxPcHRpb25zIHtcbiAgLyoqIE1hcmtkb3duIGJvZHkuIFJlbmRlcmVkIHRvIEhUTUwsIHRoZW4gd3JhcHBlZCBpbiB0aGUgbGF5b3V0LiAqL1xuICBib2R5OiBzdHJpbmc7XG4gIC8qKiBIZWFkZXItYmFuZCB0aXRsZSBhbmQgZG9jdW1lbnQgYDx0aXRsZT5gLiBEZWZhdWx0cyB0byBcIk1lc3NhZ2VcIi4gKi9cbiAgc3ViamVjdD86IHN0cmluZztcbiAgLyoqXG4gICAqIE9wdGlvbmFsIGBbbGFiZWwsIHZhbHVlXWAgZW52ZWxvcGUgcm93cyBzaG93biBhYm92ZSB0aGUgYm9keSAodXNlZFxuICAgKiBieSB0aGUgb3V0Ym94IHByZXZpZXc7IG9taXR0ZWQgZm9yIFNNVFAgc2VuZHMsIHdoZXJlIHRoZSBtYWlsIGNsaWVudFxuICAgKiBzaG93cyB0aGUgZW52ZWxvcGUgaXRzZWxmKS5cbiAgICovXG4gIGhlYWRlcnM/OiBSZWFkb25seUFycmF5PHJlYWRvbmx5IFtzdHJpbmcsIHN0cmluZ10+O1xuICAvKiogT3B0aW9uYWwgc21hbGwtcHJpbnQgZm9vdGVyIGxpbmUuIE9taXR0ZWQgd2hlbiB1bnNldC4gKi9cbiAgZm9vdGVyPzogc3RyaW5nO1xuICAvKipcbiAgICogT3B0aW9uYWwgYnJhbmQgc3R5bGluZyBmb3IgdGhlIGxheW91dCAoYWNjZW50LCBmb250LCBoZWFkZXIgbG9nbykuXG4gICAqIE9taXQgZm9yIHRoZSBuZXV0cmFsIGRlZmF1bHQgcGFsZXR0ZS5cbiAgICovXG4gIGJyYW5kPzogRW1haWxCcmFuZDtcbn1cblxuLyoqIFJlbmRlciB0aGUgb3B0aW9uYWwgZW52ZWxvcGUtaGVhZGVyIHRhYmxlIGJsb2NrLiAqL1xuZnVuY3Rpb24gbWV0YUJsb2NrKGhlYWRlcnM6IEVtYWlsSHRtbE9wdGlvbnNbXCJoZWFkZXJzXCJdKTogc3RyaW5nIHtcbiAgaWYgKCFoZWFkZXJzIHx8IGhlYWRlcnMubGVuZ3RoID09PSAwKSByZXR1cm4gXCJcIjtcbiAgY29uc3Qgcm93cyA9IGhlYWRlcnNcbiAgICAubWFwKChbbGFiZWwsIHZhbHVlXSkgPT4gYDx0cj48dGg+JHtlc2NhcGVIdG1sKGxhYmVsKX08L3RoPjx0ZD4ke2VzY2FwZUh0bWwodmFsdWUpfTwvdGQ+PC90cj5gKVxuICAgIC5qb2luKFwiXCIpO1xuICByZXR1cm4gYDx0YWJsZSByb2xlPVwicHJlc2VudGF0aW9uXCIgY2xhc3M9XCJtZXRhXCI+PHRib2R5PiR7cm93c308L3Rib2R5PjwvdGFibGU+YDtcbn1cblxuLyoqIFJlbmRlciB0aGUgb3B0aW9uYWwgZm9vdGVyIHJvdy4gKi9cbmZ1bmN0aW9uIGZvb3RlclJvdyhmb290ZXI6IHN0cmluZyB8IHVuZGVmaW5lZCk6IHN0cmluZyB7XG4gIGlmICghZm9vdGVyKSByZXR1cm4gXCJcIjtcbiAgcmV0dXJuIGBcbiAgICAgICAgICA8dHI+XG4gICAgICAgICAgICA8dGQgY2xhc3M9XCJndXR0ZXJcIiBzdHlsZT1cInBhZGRpbmc6IDE2cHggMzJweDsgYm9yZGVyLXRvcDogMXB4IHNvbGlkICNlYWVjZWY7IGNvbG9yOiAjOWFhMGE2OyBmb250LXNpemU6IDEycHg7IGxpbmUtaGVpZ2h0OiAxLjU7XCI+XG4gICAgICAgICAgICAgICR7ZXNjYXBlSHRtbChmb290ZXIpfVxuICAgICAgICAgICAgPC90ZD5cbiAgICAgICAgICA8L3RyPmA7XG59XG5cbi8qKlxuICogUmVuZGVyIHRoZSBoZWFkZXItYmFuZCBjb250ZW50OiB0aGUgYnJhbmQgbG9nbyAod2hlbiB0aGUgYnJhbmQgc3VwcGxpZXMgYVxuICogcmVuZGVyYWJsZSBpbWFnZSkgYWJvdmUgdGhlIHRpdGxlLCBvciBqdXN0IHRoZSB0aXRsZS4gVGhlIGxvZ28gaXMgY2FwcGVkXG4gKiBhdCB7QGxpbmsgTE9HT19IRUlHSFRfUFh9IGFuZCB0aW50ZWQgaW1wbGljaXRseSBieSBpdHMgb3duIGFydHdvcms7IHRoZVxuICogdGl0bGUgYWx3YXlzIHNob3dzIHNvIHRoZSBiYW5kIGlzIG5ldmVyIGVtcHR5LlxuICovXG5mdW5jdGlvbiBoZWFkZXJCYW5kKHRpdGxlOiBzdHJpbmcsIGJyYW5kOiBFbWFpbEJyYW5kLCBvbkFjY2VudDogc3RyaW5nKTogc3RyaW5nIHtcbiAgY29uc3QgbG9nbyA9IGJyYW5kLmxvZ29VcmxcbiAgICA/IGA8aW1nIHNyYz1cIiR7ZXNjYXBlSHRtbChicmFuZC5sb2dvVXJsKX1cIiBhbHQ9XCIke2VzY2FwZUh0bWwoYnJhbmQubmFtZSA/PyB0aXRsZSl9XCIgaGVpZ2h0PVwiJHtMT0dPX0hFSUdIVF9QWH1cIiBzdHlsZT1cImhlaWdodDogJHtMT0dPX0hFSUdIVF9QWH1weDsgd2lkdGg6IGF1dG87IGRpc3BsYXk6IGJsb2NrOyBtYXJnaW4tYm90dG9tOiA4cHg7XCIgLz5gXG4gICAgOiBcIlwiO1xuICByZXR1cm4gYCR7bG9nb308c3BhbiBzdHlsZT1cImNvbG9yOiAke29uQWNjZW50fTsgZm9udC1zaXplOiAxOHB4OyBmb250LXdlaWdodDogNzAwOyBsaW5lLWhlaWdodDogMS4zO1wiPiR7ZXNjYXBlSHRtbCh0aXRsZSl9PC9zcGFuPmA7XG59XG5cbi8qKlxuICogUmVuZGVyIGBib2R5YCAobWFya2Rvd24pIGludG8gYSBjb21wbGV0ZSwgc3R5bGUtaW5saW5lZCBlbWFpbCBkb2N1bWVudFxuICogdXNpbmcgdGhlIHJlc3BvbnNpdmUgbGF5b3V0LiBXaGVuIGBvcHRzLmJyYW5kYCBpcyBzZXQgaXRzIGFjY2VudCwgZm9udCxcbiAqIGFuZCBsb2dvIHN0eWxlIHRoZSBsYXlvdXQ7IG90aGVyd2lzZSBhIG5ldXRyYWwgZGVmYXVsdCBwYWxldHRlIGlzIHVzZWQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZW5kZXJFbWFpbEh0bWwob3B0czogRW1haWxIdG1sT3B0aW9ucyk6IHN0cmluZyB7XG4gIGNvbnN0IHRpdGxlID0gb3B0cy5zdWJqZWN0Py50cmltKCkgfHwgXCJNZXNzYWdlXCI7XG4gIGNvbnN0IGFjY2VudCA9IG9wdHMuYnJhbmQ/LmFjY2VudCA/PyBERUZBVUxUX0JSQU5ELmFjY2VudDtcbiAgY29uc3Qgb25BY2NlbnQgPSBvcHRzLmJyYW5kPy5vbkFjY2VudCA/PyBERUZBVUxUX0JSQU5ELm9uQWNjZW50O1xuICBjb25zdCBmb250RmFtaWx5ID0gb3B0cy5icmFuZD8uZm9udEZhbWlseSA/PyBERUZBVUxUX0JSQU5ELmZvbnRGYW1pbHk7XG4gIGNvbnN0IGJyYW5kOiBFbWFpbEJyYW5kID0gb3B0cy5icmFuZCA/PyB7IGFjY2VudCwgb25BY2NlbnQsIGZvbnRGYW1pbHkgfTtcbiAgY29uc3QgZG9jID0gYDwhZG9jdHlwZSBodG1sPlxuPGh0bWwgbGFuZz1cImVuXCI+XG4gIDxoZWFkPlxuICAgIDxtZXRhIGNoYXJzZXQ9XCJ1dGYtOFwiIC8+XG4gICAgPG1ldGEgbmFtZT1cInZpZXdwb3J0XCIgY29udGVudD1cIndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xXCIgLz5cbiAgICA8bWV0YSBuYW1lPVwiY29sb3Itc2NoZW1lXCIgY29udGVudD1cImxpZ2h0IG9ubHlcIiAvPlxuICAgIDx0aXRsZT4ke2VzY2FwZUh0bWwodGl0bGUpfTwvdGl0bGU+XG4gICAgPHN0eWxlPiR7Y29udGVudENzcyhhY2NlbnQpfVxuICAgIDwvc3R5bGU+XG4gIDwvaGVhZD5cbiAgPGJvZHkgc3R5bGU9XCJtYXJnaW46IDA7IHBhZGRpbmc6IDA7IGJhY2tncm91bmQtY29sb3I6ICNmNGY1Zjc7IC13ZWJraXQtdGV4dC1zaXplLWFkanVzdDogMTAwJTtcIj5cbiAgICA8dGFibGUgcm9sZT1cInByZXNlbnRhdGlvblwiIHdpZHRoPVwiMTAwJVwiIGNlbGxwYWRkaW5nPVwiMFwiIGNlbGxzcGFjaW5nPVwiMFwiIHN0eWxlPVwiYmFja2dyb3VuZC1jb2xvcjogI2Y0ZjVmNztcIj5cbiAgICAgIDx0cj5cbiAgICAgICAgPHRkIGFsaWduPVwiY2VudGVyXCIgc3R5bGU9XCJwYWRkaW5nOiAyNHB4IDEycHg7XCI+XG4gICAgICAgICAgPHRhYmxlIHJvbGU9XCJwcmVzZW50YXRpb25cIiBjbGFzcz1cImNvbnRhaW5lclwiIHdpZHRoPVwiNjAwXCIgY2VsbHBhZGRpbmc9XCIwXCIgY2VsbHNwYWNpbmc9XCIwXCIgc3R5bGU9XCJ3aWR0aDogNjAwcHg7IG1heC13aWR0aDogMTAwJTsgYmFja2dyb3VuZC1jb2xvcjogI2ZmZmZmZjsgYm9yZGVyLXJhZGl1czogMTBweDsgb3ZlcmZsb3c6IGhpZGRlbjsgYm94LXNoYWRvdzogMCAxcHggM3B4IHJnYmEoMCwwLDAsMC4wOCk7IGZvbnQtZmFtaWx5OiAke2ZvbnRGYW1pbHl9O1wiPlxuICAgICAgICAgICAgPHRyPlxuICAgICAgICAgICAgICA8dGQgY2xhc3M9XCJndXR0ZXJcIiBzdHlsZT1cInBhZGRpbmc6IDIwcHggMzJweDsgYmFja2dyb3VuZC1jb2xvcjogJHthY2NlbnR9O1wiPlxuICAgICAgICAgICAgICAgICR7aGVhZGVyQmFuZCh0aXRsZSwgYnJhbmQsIG9uQWNjZW50KX1cbiAgICAgICAgICAgICAgPC90ZD5cbiAgICAgICAgICAgIDwvdHI+XG4gICAgICAgICAgICA8dHI+XG4gICAgICAgICAgICAgIDx0ZCBjbGFzcz1cImd1dHRlclwiIHN0eWxlPVwicGFkZGluZzogMjRweCAzMnB4IDhweDtcIj5cbiAgICAgICAgICAgICAgICAke21ldGFCbG9jayhvcHRzLmhlYWRlcnMpfVxuICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9XCJlbWFpbC1ib2R5XCI+JHttYXJrZG93blRvSHRtbChvcHRzLmJvZHkpfTwvZGl2PlxuICAgICAgICAgICAgICA8L3RkPlxuICAgICAgICAgICAgPC90cj4ke2Zvb3RlclJvdyhvcHRzLmZvb3Rlcil9XG4gICAgICAgICAgPC90YWJsZT5cbiAgICAgICAgPC90ZD5cbiAgICAgIDwvdHI+XG4gICAgPC90YWJsZT5cbiAgPC9ib2R5PlxuPC9odG1sPlxuYDtcbiAgcmV0dXJuIGp1aWNlKGRvYyk7XG59XG4iXX0=
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Server-side markdown -> HTML rendering for email bodies. The model
3
+ * drafts bodies in markdown; this turns them into real HTML (GFM tables,
4
+ * lists, code, links). {@link normalizeMarkdown} first repairs the two
5
+ * structures LLMs most often emit as plain text instead of markdown -
6
+ * `=====` divider rules and pipe tables missing their `| --- |`
7
+ * separator row - so they render as a `<hr>` / `<table>` rather than
8
+ * literal text. The prompt steers the model away from ASCII art; this is
9
+ * the belt-and-suspenders fallback.
10
+ *
11
+ * @module
12
+ */
13
+ /**
14
+ * Repair common LLM "looks-like-markdown-but-isn't" output: convert
15
+ * standalone `=====` / `_____` rules to a `---` thematic break (leaving
16
+ * genuine setext-heading underlines intact), and insert a `| --- |`
17
+ * separator after the first row of a pipe block that lacks one so GFM
18
+ * renders it as a table.
19
+ */
20
+ export declare function normalizeMarkdown(src: string): string;
21
+ /** Render a markdown body to an HTML fragment (GFM tables enabled). */
22
+ export declare function markdownToHtml(body: string): string;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Server-side markdown -> HTML rendering for email bodies. The model
3
+ * drafts bodies in markdown; this turns them into real HTML (GFM tables,
4
+ * lists, code, links). {@link normalizeMarkdown} first repairs the two
5
+ * structures LLMs most often emit as plain text instead of markdown -
6
+ * `=====` divider rules and pipe tables missing their `| --- |`
7
+ * separator row - so they render as a `<hr>` / `<table>` rather than
8
+ * literal text. The prompt steers the model away from ASCII art; this is
9
+ * the belt-and-suspenders fallback.
10
+ *
11
+ * @module
12
+ */
13
+ import { marked } from "marked";
14
+ /** A line of only `=` or `_` (length >= 3): an ASCII divider rule. */
15
+ function isAsciiRule(line) {
16
+ return /^[ \t]*[=_]{3,}[ \t]*$/.test(line);
17
+ }
18
+ /** A line that participates in a markdown pipe table (has a `|`). */
19
+ function isPipeRow(line) {
20
+ return line.includes("|") && line.trim().length > 0;
21
+ }
22
+ /** A markdown table separator row, e.g. `| --- | :--: |`. */
23
+ function isSeparatorRow(line) {
24
+ return /^[ \t]*\|?[ \t]*:?-{2,}:?[ \t]*(\|[ \t]*:?-{2,}:?[ \t]*)+\|?[ \t]*$/.test(line);
25
+ }
26
+ /** Column count of a pipe row (outer pipes optional). */
27
+ function pipeColumns(line) {
28
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").length;
29
+ }
30
+ /** A GFM separator row with `columns` cells. */
31
+ function separatorRow(columns) {
32
+ return `| ${Array.from({ length: columns }, () => "---").join(" | ")} |`;
33
+ }
34
+ /**
35
+ * Repair common LLM "looks-like-markdown-but-isn't" output: convert
36
+ * standalone `=====` / `_____` rules to a `---` thematic break (leaving
37
+ * genuine setext-heading underlines intact), and insert a `| --- |`
38
+ * separator after the first row of a pipe block that lacks one so GFM
39
+ * renders it as a table.
40
+ */
41
+ export function normalizeMarkdown(src) {
42
+ const lines = src.split("\n");
43
+ const out = [];
44
+ for (let i = 0; i < lines.length; i++) {
45
+ const line = lines[i];
46
+ const prev = i > 0 ? lines[i - 1] : "";
47
+ const next = i + 1 < lines.length ? lines[i + 1] : "";
48
+ if (isAsciiRule(line)) {
49
+ // A run of `=` directly under a non-blank text line is a setext H1
50
+ // underline - keep it. Anything else is a decorative divider.
51
+ const isSetextUnderline = /^[ \t]*={3,}[ \t]*$/.test(line) && prev.trim() !== "" && !isPipeRow(prev);
52
+ out.push(isSetextUnderline ? line : "---");
53
+ continue;
54
+ }
55
+ // First row of a pipe block (prev is not itself a pipe row) followed
56
+ // by another aligned pipe row, with no separator and no `###` bar-
57
+ // chart fill: treat as a table header and inject the separator.
58
+ const startsPipeBlock = isPipeRow(line) && !isPipeRow(prev);
59
+ if (startsPipeBlock &&
60
+ isPipeRow(next) &&
61
+ !isSeparatorRow(next) &&
62
+ !/#{3,}/.test(line) &&
63
+ pipeColumns(line) >= 2 &&
64
+ pipeColumns(line) === pipeColumns(next)) {
65
+ out.push(line);
66
+ out.push(separatorRow(pipeColumns(line)));
67
+ continue;
68
+ }
69
+ out.push(line);
70
+ }
71
+ return out.join("\n");
72
+ }
73
+ /** Render a markdown body to an HTML fragment (GFM tables enabled). */
74
+ export function markdownToHtml(body) {
75
+ return marked.parse(normalizeMarkdown(body), {
76
+ async: false,
77
+ gfm: true,
78
+ breaks: true,
79
+ });
80
+ }
81
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFya2Rvd24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvbWFya2Rvd24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7O0dBV0c7QUFFSCxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBRWhDLHNFQUFzRTtBQUN0RSxTQUFTLFdBQVcsQ0FBQyxJQUFZO0lBQy9CLE9BQU8sd0JBQXdCLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0FBQzdDLENBQUM7QUFFRCxxRUFBcUU7QUFDckUsU0FBUyxTQUFTLENBQUMsSUFBWTtJQUM3QixPQUFPLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDLElBQUksRUFBRSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUM7QUFDdEQsQ0FBQztBQUVELDZEQUE2RDtBQUM3RCxTQUFTLGNBQWMsQ0FBQyxJQUFZO0lBQ2xDLE9BQU8scUVBQXFFLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0FBQzFGLENBQUM7QUFFRCx5REFBeUQ7QUFDekQsU0FBUyxXQUFXLENBQUMsSUFBWTtJQUMvQixPQUFPLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sQ0FBQztBQUM3RSxDQUFDO0FBRUQsZ0RBQWdEO0FBQ2hELFNBQVMsWUFBWSxDQUFDLE9BQWU7SUFDbkMsT0FBTyxLQUFLLEtBQUssQ0FBQyxJQUFJLENBQUMsRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUFFLEVBQUUsR0FBRyxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUM7QUFDM0UsQ0FBQztBQUVEOzs7Ozs7R0FNRztBQUNILE1BQU0sVUFBVSxpQkFBaUIsQ0FBQyxHQUFXO0lBQzNDLE1BQU0sS0FBSyxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDOUIsTUFBTSxHQUFHLEdBQWEsRUFBRSxDQUFDO0lBQ3pCLEtBQUssSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsR0FBRyxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUMsRUFBRSxFQUFFLENBQUM7UUFDdEMsTUFBTSxJQUFJLEdBQUcsS0FBSyxDQUFDLENBQUMsQ0FBRSxDQUFDO1FBQ3ZCLE1BQU0sSUFBSSxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUN4QyxNQUFNLElBQUksR0FBRyxDQUFDLEdBQUcsQ0FBQyxHQUFHLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUV2RCxJQUFJLFdBQVcsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDO1lBQ3RCLG1FQUFtRTtZQUNuRSw4REFBOEQ7WUFDOUQsTUFBTSxpQkFBaUIsR0FDckIscUJBQXFCLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLElBQUksQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDN0UsR0FBRyxDQUFDLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQztZQUMzQyxTQUFTO1FBQ1gsQ0FBQztRQUVELHFFQUFxRTtRQUNyRSxtRUFBbUU7UUFDbkUsZ0VBQWdFO1FBQ2hFLE1BQU0sZUFBZSxHQUFHLFNBQVMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUM1RCxJQUNFLGVBQWU7WUFDZixTQUFTLENBQUMsSUFBSSxDQUFDO1lBQ2YsQ0FBQyxjQUFjLENBQUMsSUFBSSxDQUFDO1lBQ3JCLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7WUFDbkIsV0FBVyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7WUFDdEIsV0FBVyxDQUFDLElBQUksQ0FBQyxLQUFLLFdBQVcsQ0FBQyxJQUFJLENBQUMsRUFDdkMsQ0FBQztZQUNELEdBQUcsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDZixHQUFHLENBQUMsSUFBSSxDQUFDLFlBQVksQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQzFDLFNBQVM7UUFDWCxDQUFDO1FBRUQsR0FBRyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUNqQixDQUFDO0lBQ0QsT0FBTyxHQUFHLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0FBQ3hCLENBQUM7QUFFRCx1RUFBdUU7QUFDdkUsTUFBTSxVQUFVLGNBQWMsQ0FBQyxJQUFZO0lBQ3pDLE9BQU8sTUFBTSxDQUFDLEtBQUssQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsRUFBRTtRQUMzQyxLQUFLLEVBQUUsS0FBSztRQUNaLEdBQUcsRUFBRSxJQUFJO1FBQ1QsTUFBTSxFQUFFLElBQUk7S0FDYixDQUFDLENBQUM7QUFDTCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTZXJ2ZXItc2lkZSBtYXJrZG93biAtPiBIVE1MIHJlbmRlcmluZyBmb3IgZW1haWwgYm9kaWVzLiBUaGUgbW9kZWxcbiAqIGRyYWZ0cyBib2RpZXMgaW4gbWFya2Rvd247IHRoaXMgdHVybnMgdGhlbSBpbnRvIHJlYWwgSFRNTCAoR0ZNIHRhYmxlcyxcbiAqIGxpc3RzLCBjb2RlLCBsaW5rcykuIHtAbGluayBub3JtYWxpemVNYXJrZG93bn0gZmlyc3QgcmVwYWlycyB0aGUgdHdvXG4gKiBzdHJ1Y3R1cmVzIExMTXMgbW9zdCBvZnRlbiBlbWl0IGFzIHBsYWluIHRleHQgaW5zdGVhZCBvZiBtYXJrZG93biAtXG4gKiBgPT09PT1gIGRpdmlkZXIgcnVsZXMgYW5kIHBpcGUgdGFibGVzIG1pc3NpbmcgdGhlaXIgYHwgLS0tIHxgXG4gKiBzZXBhcmF0b3Igcm93IC0gc28gdGhleSByZW5kZXIgYXMgYSBgPGhyPmAgLyBgPHRhYmxlPmAgcmF0aGVyIHRoYW5cbiAqIGxpdGVyYWwgdGV4dC4gVGhlIHByb21wdCBzdGVlcnMgdGhlIG1vZGVsIGF3YXkgZnJvbSBBU0NJSSBhcnQ7IHRoaXMgaXNcbiAqIHRoZSBiZWx0LWFuZC1zdXNwZW5kZXJzIGZhbGxiYWNrLlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuXG5pbXBvcnQgeyBtYXJrZWQgfSBmcm9tIFwibWFya2VkXCI7XG5cbi8qKiBBIGxpbmUgb2Ygb25seSBgPWAgb3IgYF9gIChsZW5ndGggPj0gMyk6IGFuIEFTQ0lJIGRpdmlkZXIgcnVsZS4gKi9cbmZ1bmN0aW9uIGlzQXNjaWlSdWxlKGxpbmU6IHN0cmluZyk6IGJvb2xlYW4ge1xuICByZXR1cm4gL15bIFxcdF0qWz1fXXszLH1bIFxcdF0qJC8udGVzdChsaW5lKTtcbn1cblxuLyoqIEEgbGluZSB0aGF0IHBhcnRpY2lwYXRlcyBpbiBhIG1hcmtkb3duIHBpcGUgdGFibGUgKGhhcyBhIGB8YCkuICovXG5mdW5jdGlvbiBpc1BpcGVSb3cobGluZTogc3RyaW5nKTogYm9vbGVhbiB7XG4gIHJldHVybiBsaW5lLmluY2x1ZGVzKFwifFwiKSAmJiBsaW5lLnRyaW0oKS5sZW5ndGggPiAwO1xufVxuXG4vKiogQSBtYXJrZG93biB0YWJsZSBzZXBhcmF0b3Igcm93LCBlLmcuIGB8IC0tLSB8IDotLTogfGAuICovXG5mdW5jdGlvbiBpc1NlcGFyYXRvclJvdyhsaW5lOiBzdHJpbmcpOiBib29sZWFuIHtcbiAgcmV0dXJuIC9eWyBcXHRdKlxcfD9bIFxcdF0qOj8tezIsfTo/WyBcXHRdKihcXHxbIFxcdF0qOj8tezIsfTo/WyBcXHRdKikrXFx8P1sgXFx0XSokLy50ZXN0KGxpbmUpO1xufVxuXG4vKiogQ29sdW1uIGNvdW50IG9mIGEgcGlwZSByb3cgKG91dGVyIHBpcGVzIG9wdGlvbmFsKS4gKi9cbmZ1bmN0aW9uIHBpcGVDb2x1bW5zKGxpbmU6IHN0cmluZyk6IG51bWJlciB7XG4gIHJldHVybiBsaW5lLnRyaW0oKS5yZXBsYWNlKC9eXFx8LywgXCJcIikucmVwbGFjZSgvXFx8JC8sIFwiXCIpLnNwbGl0KFwifFwiKS5sZW5ndGg7XG59XG5cbi8qKiBBIEdGTSBzZXBhcmF0b3Igcm93IHdpdGggYGNvbHVtbnNgIGNlbGxzLiAqL1xuZnVuY3Rpb24gc2VwYXJhdG9yUm93KGNvbHVtbnM6IG51bWJlcik6IHN0cmluZyB7XG4gIHJldHVybiBgfCAke0FycmF5LmZyb20oeyBsZW5ndGg6IGNvbHVtbnMgfSwgKCkgPT4gXCItLS1cIikuam9pbihcIiB8IFwiKX0gfGA7XG59XG5cbi8qKlxuICogUmVwYWlyIGNvbW1vbiBMTE0gXCJsb29rcy1saWtlLW1hcmtkb3duLWJ1dC1pc24ndFwiIG91dHB1dDogY29udmVydFxuICogc3RhbmRhbG9uZSBgPT09PT1gIC8gYF9fX19fYCBydWxlcyB0byBhIGAtLS1gIHRoZW1hdGljIGJyZWFrIChsZWF2aW5nXG4gKiBnZW51aW5lIHNldGV4dC1oZWFkaW5nIHVuZGVybGluZXMgaW50YWN0KSwgYW5kIGluc2VydCBhIGB8IC0tLSB8YFxuICogc2VwYXJhdG9yIGFmdGVyIHRoZSBmaXJzdCByb3cgb2YgYSBwaXBlIGJsb2NrIHRoYXQgbGFja3Mgb25lIHNvIEdGTVxuICogcmVuZGVycyBpdCBhcyBhIHRhYmxlLlxuICovXG5leHBvcnQgZnVuY3Rpb24gbm9ybWFsaXplTWFya2Rvd24oc3JjOiBzdHJpbmcpOiBzdHJpbmcge1xuICBjb25zdCBsaW5lcyA9IHNyYy5zcGxpdChcIlxcblwiKTtcbiAgY29uc3Qgb3V0OiBzdHJpbmdbXSA9IFtdO1xuICBmb3IgKGxldCBpID0gMDsgaSA8IGxpbmVzLmxlbmd0aDsgaSsrKSB7XG4gICAgY29uc3QgbGluZSA9IGxpbmVzW2ldITtcbiAgICBjb25zdCBwcmV2ID0gaSA+IDAgPyBsaW5lc1tpIC0gMV0hIDogXCJcIjtcbiAgICBjb25zdCBuZXh0ID0gaSArIDEgPCBsaW5lcy5sZW5ndGggPyBsaW5lc1tpICsgMV0hIDogXCJcIjtcblxuICAgIGlmIChpc0FzY2lpUnVsZShsaW5lKSkge1xuICAgICAgLy8gQSBydW4gb2YgYD1gIGRpcmVjdGx5IHVuZGVyIGEgbm9uLWJsYW5rIHRleHQgbGluZSBpcyBhIHNldGV4dCBIMVxuICAgICAgLy8gdW5kZXJsaW5lIC0ga2VlcCBpdC4gQW55dGhpbmcgZWxzZSBpcyBhIGRlY29yYXRpdmUgZGl2aWRlci5cbiAgICAgIGNvbnN0IGlzU2V0ZXh0VW5kZXJsaW5lID1cbiAgICAgICAgL15bIFxcdF0qPXszLH1bIFxcdF0qJC8udGVzdChsaW5lKSAmJiBwcmV2LnRyaW0oKSAhPT0gXCJcIiAmJiAhaXNQaXBlUm93KHByZXYpO1xuICAgICAgb3V0LnB1c2goaXNTZXRleHRVbmRlcmxpbmUgPyBsaW5lIDogXCItLS1cIik7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG5cbiAgICAvLyBGaXJzdCByb3cgb2YgYSBwaXBlIGJsb2NrIChwcmV2IGlzIG5vdCBpdHNlbGYgYSBwaXBlIHJvdykgZm9sbG93ZWRcbiAgICAvLyBieSBhbm90aGVyIGFsaWduZWQgcGlwZSByb3csIHdpdGggbm8gc2VwYXJhdG9yIGFuZCBubyBgIyMjYCBiYXItXG4gICAgLy8gY2hhcnQgZmlsbDogdHJlYXQgYXMgYSB0YWJsZSBoZWFkZXIgYW5kIGluamVjdCB0aGUgc2VwYXJhdG9yLlxuICAgIGNvbnN0IHN0YXJ0c1BpcGVCbG9jayA9IGlzUGlwZVJvdyhsaW5lKSAmJiAhaXNQaXBlUm93KHByZXYpO1xuICAgIGlmIChcbiAgICAgIHN0YXJ0c1BpcGVCbG9jayAmJlxuICAgICAgaXNQaXBlUm93KG5leHQpICYmXG4gICAgICAhaXNTZXBhcmF0b3JSb3cobmV4dCkgJiZcbiAgICAgICEvI3szLH0vLnRlc3QobGluZSkgJiZcbiAgICAgIHBpcGVDb2x1bW5zKGxpbmUpID49IDIgJiZcbiAgICAgIHBpcGVDb2x1bW5zKGxpbmUpID09PSBwaXBlQ29sdW1ucyhuZXh0KVxuICAgICkge1xuICAgICAgb3V0LnB1c2gobGluZSk7XG4gICAgICBvdXQucHVzaChzZXBhcmF0b3JSb3cocGlwZUNvbHVtbnMobGluZSkpKTtcbiAgICAgIGNvbnRpbnVlO1xuICAgIH1cblxuICAgIG91dC5wdXNoKGxpbmUpO1xuICB9XG4gIHJldHVybiBvdXQuam9pbihcIlxcblwiKTtcbn1cblxuLyoqIFJlbmRlciBhIG1hcmtkb3duIGJvZHkgdG8gYW4gSFRNTCBmcmFnbWVudCAoR0ZNIHRhYmxlcyBlbmFibGVkKS4gKi9cbmV4cG9ydCBmdW5jdGlvbiBtYXJrZG93blRvSHRtbChib2R5OiBzdHJpbmcpOiBzdHJpbmcge1xuICByZXR1cm4gbWFya2VkLnBhcnNlKG5vcm1hbGl6ZU1hcmtkb3duKGJvZHkpLCB7XG4gICAgYXN5bmM6IGZhbHNlLFxuICAgIGdmbTogdHJ1ZSxcbiAgICBicmVha3M6IHRydWUsXG4gIH0pO1xufVxuIl19
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Filesystem outbox for the no-SMTP fallback: {@link writeOutboxEmail}
3
+ * persists one drafted message as a standalone HTML file under
4
+ * `<dir>/<from>/<timestamp>-<subject-slug>.html` instead of sending it.
5
+ * This lets the approval flow and agents be exercised end-to-end with
6
+ * zero email configuration - open the file in a browser to see exactly
7
+ * what would have gone out (the HTML matches the SMTP send, headers
8
+ * table aside). The body is rendered + style-inlined by
9
+ * {@link renderEmailHtml}. Attachments are listed by filename in the
10
+ * header table but not written to disk - the outbox previews the
11
+ * envelope, it doesn't reproduce the wire payload.
12
+ *
13
+ * @module
14
+ */
15
+ import type { EmailMessage } from "@dbx-tools/shared-email";
16
+ import type { EmailBrand } from "./brand.js";
17
+ /**
18
+ * Write one message as HTML under `<dir>/<from>/` and return the
19
+ * absolute path. The per-sender folder mirrors the SMTP `From`, so test
20
+ * output groups by who the message would have been sent as.
21
+ */
22
+ export declare function writeOutboxEmail(message: EmailMessage, from: string, dir: string, brand?: EmailBrand): Promise<string>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Filesystem outbox for the no-SMTP fallback: {@link writeOutboxEmail}
3
+ * persists one drafted message as a standalone HTML file under
4
+ * `<dir>/<from>/<timestamp>-<subject-slug>.html` instead of sending it.
5
+ * This lets the approval flow and agents be exercised end-to-end with
6
+ * zero email configuration - open the file in a browser to see exactly
7
+ * what would have gone out (the HTML matches the SMTP send, headers
8
+ * table aside). The body is rendered + style-inlined by
9
+ * {@link renderEmailHtml}. Attachments are listed by filename in the
10
+ * header table but not written to disk - the outbox previews the
11
+ * envelope, it doesn't reproduce the wire payload.
12
+ *
13
+ * @module
14
+ */
15
+ import { mkdir, writeFile } from "node:fs/promises";
16
+ import { join, resolve } from "node:path";
17
+ import { string } from "@dbx-tools/shared-core";
18
+ import { renderEmailHtml } from "./email-html.js";
19
+ /**
20
+ * Longest subject slug kept in an outbox file name, leaving room for the
21
+ * timestamp prefix and extension inside common filesystem name limits.
22
+ */
23
+ const SUBJECT_SLUG_MAX_LENGTH = 48;
24
+ /** Filesystem-safe slug of the subject for the file name. */
25
+ function subjectSlug(subject) {
26
+ return string.toSlugWithOptions({ maxLength: SUBJECT_SLUG_MAX_LENGTH }, subject) || "email";
27
+ }
28
+ /** The envelope rows shown above the body in the preview file. */
29
+ function headerRows(message, from) {
30
+ const rows = [
31
+ ["From", from],
32
+ ["To", message.to.join(", ")],
33
+ ["Cc", message.cc?.join(", ")],
34
+ ["Bcc", message.bcc?.join(", ")],
35
+ ["Subject", message.subject],
36
+ ["Attachments", message.attachments?.map((att) => att.filename).join(", ")],
37
+ ["Date", new Date().toISOString()],
38
+ ];
39
+ return rows.filter((row) => Boolean(row[1]));
40
+ }
41
+ /**
42
+ * Write one message as HTML under `<dir>/<from>/` and return the
43
+ * absolute path. The per-sender folder mirrors the SMTP `From`, so test
44
+ * output groups by who the message would have been sent as.
45
+ */
46
+ export async function writeOutboxEmail(message, from, dir, brand) {
47
+ const folder = resolve(dir, from);
48
+ await mkdir(folder, { recursive: true });
49
+ const path = join(folder, `${Date.now()}-${subjectSlug(message.subject)}.html`);
50
+ const html = renderEmailHtml({
51
+ subject: message.subject,
52
+ headers: headerRows(message, from),
53
+ body: message.body,
54
+ footer: "Local outbox preview - written to disk, not sent (no SMTP credentials configured).",
55
+ ...(brand ? { brand } : {}),
56
+ });
57
+ await writeFile(path, html, "utf8");
58
+ return path;
59
+ }
60
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3V0Ym94LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL291dGJveC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7OztHQWFHO0FBRUgsT0FBTyxFQUFFLEtBQUssRUFBRSxTQUFTLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUNwRCxPQUFPLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUMxQyxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFHaEQsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUUvQzs7O0dBR0c7QUFDSCxNQUFNLHVCQUF1QixHQUFHLEVBQUUsQ0FBQztBQUVuQyw2REFBNkQ7QUFDN0QsU0FBUyxXQUFXLENBQUMsT0FBZTtJQUNsQyxPQUFPLE1BQU0sQ0FBQyxpQkFBaUIsQ0FBQyxFQUFFLFNBQVMsRUFBRSx1QkFBdUIsRUFBRSxFQUFFLE9BQU8sQ0FBQyxJQUFJLE9BQU8sQ0FBQztBQUM5RixDQUFDO0FBRUQsa0VBQWtFO0FBQ2xFLFNBQVMsVUFBVSxDQUFDLE9BQXFCLEVBQUUsSUFBWTtJQUNyRCxNQUFNLElBQUksR0FBaUQ7UUFDekQsQ0FBQyxNQUFNLEVBQUUsSUFBSSxDQUFDO1FBQ2QsQ0FBQyxJQUFJLEVBQUUsT0FBTyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDN0IsQ0FBQyxJQUFJLEVBQUUsT0FBTyxDQUFDLEVBQUUsRUFBRSxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDOUIsQ0FBQyxLQUFLLEVBQUUsT0FBTyxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDaEMsQ0FBQyxTQUFTLEVBQUUsT0FBTyxDQUFDLE9BQU8sQ0FBQztRQUM1QixDQUFDLGFBQWEsRUFBRSxPQUFPLENBQUMsV0FBVyxFQUFFLEdBQUcsQ0FBQyxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUMzRSxDQUFDLE1BQU0sRUFBRSxJQUFJLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRSxDQUFDO0tBQ25DLENBQUM7SUFDRixPQUFPLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxHQUFHLEVBQTJCLEVBQUUsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUN4RSxDQUFDO0FBRUQ7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsZ0JBQWdCLENBQ3BDLE9BQXFCLEVBQ3JCLElBQVksRUFDWixHQUFXLEVBQ1gsS0FBa0I7SUFFbEIsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsQ0FBQztJQUNsQyxNQUFNLEtBQUssQ0FBQyxNQUFNLEVBQUUsRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUN6QyxNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsTUFBTSxFQUFFLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxJQUFJLFdBQVcsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ2hGLE1BQU0sSUFBSSxHQUFHLGVBQWUsQ0FBQztRQUMzQixPQUFPLEVBQUUsT0FBTyxDQUFDLE9BQU87UUFDeEIsT0FBTyxFQUFFLFVBQVUsQ0FBQyxPQUFPLEVBQUUsSUFBSSxDQUFDO1FBQ2xDLElBQUksRUFBRSxPQUFPLENBQUMsSUFBSTtRQUNsQixNQUFNLEVBQUUsb0ZBQW9GO1FBQzVGLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztLQUM1QixDQUFDLENBQUM7SUFDSCxNQUFNLFNBQVMsQ0FBQyxJQUFJLEVBQUUsSUFBSSxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQ3BDLE9BQU8sSUFBSSxDQUFDO0FBQ2QsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogRmlsZXN5c3RlbSBvdXRib3ggZm9yIHRoZSBuby1TTVRQIGZhbGxiYWNrOiB7QGxpbmsgd3JpdGVPdXRib3hFbWFpbH1cbiAqIHBlcnNpc3RzIG9uZSBkcmFmdGVkIG1lc3NhZ2UgYXMgYSBzdGFuZGFsb25lIEhUTUwgZmlsZSB1bmRlclxuICogYDxkaXI+Lzxmcm9tPi88dGltZXN0YW1wPi08c3ViamVjdC1zbHVnPi5odG1sYCBpbnN0ZWFkIG9mIHNlbmRpbmcgaXQuXG4gKiBUaGlzIGxldHMgdGhlIGFwcHJvdmFsIGZsb3cgYW5kIGFnZW50cyBiZSBleGVyY2lzZWQgZW5kLXRvLWVuZCB3aXRoXG4gKiB6ZXJvIGVtYWlsIGNvbmZpZ3VyYXRpb24gLSBvcGVuIHRoZSBmaWxlIGluIGEgYnJvd3NlciB0byBzZWUgZXhhY3RseVxuICogd2hhdCB3b3VsZCBoYXZlIGdvbmUgb3V0ICh0aGUgSFRNTCBtYXRjaGVzIHRoZSBTTVRQIHNlbmQsIGhlYWRlcnNcbiAqIHRhYmxlIGFzaWRlKS4gVGhlIGJvZHkgaXMgcmVuZGVyZWQgKyBzdHlsZS1pbmxpbmVkIGJ5XG4gKiB7QGxpbmsgcmVuZGVyRW1haWxIdG1sfS4gQXR0YWNobWVudHMgYXJlIGxpc3RlZCBieSBmaWxlbmFtZSBpbiB0aGVcbiAqIGhlYWRlciB0YWJsZSBidXQgbm90IHdyaXR0ZW4gdG8gZGlzayAtIHRoZSBvdXRib3ggcHJldmlld3MgdGhlXG4gKiBlbnZlbG9wZSwgaXQgZG9lc24ndCByZXByb2R1Y2UgdGhlIHdpcmUgcGF5bG9hZC5cbiAqXG4gKiBAbW9kdWxlXG4gKi9cblxuaW1wb3J0IHsgbWtkaXIsIHdyaXRlRmlsZSB9IGZyb20gXCJub2RlOmZzL3Byb21pc2VzXCI7XG5pbXBvcnQgeyBqb2luLCByZXNvbHZlIH0gZnJvbSBcIm5vZGU6cGF0aFwiO1xuaW1wb3J0IHsgc3RyaW5nIH0gZnJvbSBcIkBkYngtdG9vbHMvc2hhcmVkLWNvcmVcIjtcbmltcG9ydCB0eXBlIHsgRW1haWxNZXNzYWdlIH0gZnJvbSBcIkBkYngtdG9vbHMvc2hhcmVkLWVtYWlsXCI7XG5pbXBvcnQgdHlwZSB7IEVtYWlsQnJhbmQgfSBmcm9tIFwiLi9icmFuZFwiO1xuaW1wb3J0IHsgcmVuZGVyRW1haWxIdG1sIH0gZnJvbSBcIi4vZW1haWwtaHRtbFwiO1xuXG4vKipcbiAqIExvbmdlc3Qgc3ViamVjdCBzbHVnIGtlcHQgaW4gYW4gb3V0Ym94IGZpbGUgbmFtZSwgbGVhdmluZyByb29tIGZvciB0aGVcbiAqIHRpbWVzdGFtcCBwcmVmaXggYW5kIGV4dGVuc2lvbiBpbnNpZGUgY29tbW9uIGZpbGVzeXN0ZW0gbmFtZSBsaW1pdHMuXG4gKi9cbmNvbnN0IFNVQkpFQ1RfU0xVR19NQVhfTEVOR1RIID0gNDg7XG5cbi8qKiBGaWxlc3lzdGVtLXNhZmUgc2x1ZyBvZiB0aGUgc3ViamVjdCBmb3IgdGhlIGZpbGUgbmFtZS4gKi9cbmZ1bmN0aW9uIHN1YmplY3RTbHVnKHN1YmplY3Q6IHN0cmluZyk6IHN0cmluZyB7XG4gIHJldHVybiBzdHJpbmcudG9TbHVnV2l0aE9wdGlvbnMoeyBtYXhMZW5ndGg6IFNVQkpFQ1RfU0xVR19NQVhfTEVOR1RIIH0sIHN1YmplY3QpIHx8IFwiZW1haWxcIjtcbn1cblxuLyoqIFRoZSBlbnZlbG9wZSByb3dzIHNob3duIGFib3ZlIHRoZSBib2R5IGluIHRoZSBwcmV2aWV3IGZpbGUuICovXG5mdW5jdGlvbiBoZWFkZXJSb3dzKG1lc3NhZ2U6IEVtYWlsTWVzc2FnZSwgZnJvbTogc3RyaW5nKTogQXJyYXk8cmVhZG9ubHkgW3N0cmluZywgc3RyaW5nXT4ge1xuICBjb25zdCByb3dzOiBBcnJheTxyZWFkb25seSBbc3RyaW5nLCBzdHJpbmcgfCB1bmRlZmluZWRdPiA9IFtcbiAgICBbXCJGcm9tXCIsIGZyb21dLFxuICAgIFtcIlRvXCIsIG1lc3NhZ2UudG8uam9pbihcIiwgXCIpXSxcbiAgICBbXCJDY1wiLCBtZXNzYWdlLmNjPy5qb2luKFwiLCBcIildLFxuICAgIFtcIkJjY1wiLCBtZXNzYWdlLmJjYz8uam9pbihcIiwgXCIpXSxcbiAgICBbXCJTdWJqZWN0XCIsIG1lc3NhZ2Uuc3ViamVjdF0sXG4gICAgW1wiQXR0YWNobWVudHNcIiwgbWVzc2FnZS5hdHRhY2htZW50cz8ubWFwKChhdHQpID0+IGF0dC5maWxlbmFtZSkuam9pbihcIiwgXCIpXSxcbiAgICBbXCJEYXRlXCIsIG5ldyBEYXRlKCkudG9JU09TdHJpbmcoKV0sXG4gIF07XG4gIHJldHVybiByb3dzLmZpbHRlcigocm93KTogcm93IGlzIFtzdHJpbmcsIHN0cmluZ10gPT4gQm9vbGVhbihyb3dbMV0pKTtcbn1cblxuLyoqXG4gKiBXcml0ZSBvbmUgbWVzc2FnZSBhcyBIVE1MIHVuZGVyIGA8ZGlyPi88ZnJvbT4vYCBhbmQgcmV0dXJuIHRoZVxuICogYWJzb2x1dGUgcGF0aC4gVGhlIHBlci1zZW5kZXIgZm9sZGVyIG1pcnJvcnMgdGhlIFNNVFAgYEZyb21gLCBzbyB0ZXN0XG4gKiBvdXRwdXQgZ3JvdXBzIGJ5IHdobyB0aGUgbWVzc2FnZSB3b3VsZCBoYXZlIGJlZW4gc2VudCBhcy5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHdyaXRlT3V0Ym94RW1haWwoXG4gIG1lc3NhZ2U6IEVtYWlsTWVzc2FnZSxcbiAgZnJvbTogc3RyaW5nLFxuICBkaXI6IHN0cmluZyxcbiAgYnJhbmQ/OiBFbWFpbEJyYW5kLFxuKTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgY29uc3QgZm9sZGVyID0gcmVzb2x2ZShkaXIsIGZyb20pO1xuICBhd2FpdCBta2Rpcihmb2xkZXIsIHsgcmVjdXJzaXZlOiB0cnVlIH0pO1xuICBjb25zdCBwYXRoID0gam9pbihmb2xkZXIsIGAke0RhdGUubm93KCl9LSR7c3ViamVjdFNsdWcobWVzc2FnZS5zdWJqZWN0KX0uaHRtbGApO1xuICBjb25zdCBodG1sID0gcmVuZGVyRW1haWxIdG1sKHtcbiAgICBzdWJqZWN0OiBtZXNzYWdlLnN1YmplY3QsXG4gICAgaGVhZGVyczogaGVhZGVyUm93cyhtZXNzYWdlLCBmcm9tKSxcbiAgICBib2R5OiBtZXNzYWdlLmJvZHksXG4gICAgZm9vdGVyOiBcIkxvY2FsIG91dGJveCBwcmV2aWV3IC0gd3JpdHRlbiB0byBkaXNrLCBub3Qgc2VudCAobm8gU01UUCBjcmVkZW50aWFscyBjb25maWd1cmVkKS5cIixcbiAgICAuLi4oYnJhbmQgPyB7IGJyYW5kIH0gOiB7fSksXG4gIH0pO1xuICBhd2FpdCB3cml0ZUZpbGUocGF0aCwgaHRtbCwgXCJ1dGY4XCIpO1xuICByZXR1cm4gcGF0aDtcbn1cbiJdfQ==