@aglyn/plugins-email 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +58 -0
- package/src/index.d.ts +35 -0
- package/src/index.js +35 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/campaign-design-create-widget.d.ts +21 -0
- package/src/lib/components/campaign-design-create-widget.js +61 -0
- package/src/lib/components/campaign-design-create-widget.js.map +1 -0
- package/src/lib/components/campaign-sender-editor-widget.d.ts +18 -0
- package/src/lib/components/campaign-sender-editor-widget.js +39 -0
- package/src/lib/components/campaign-sender-editor-widget.js.map +1 -0
- package/src/lib/components/campaign-topic-options-widget.d.ts +19 -0
- package/src/lib/components/campaign-topic-options-widget.js +48 -0
- package/src/lib/components/campaign-topic-options-widget.js.map +1 -0
- package/src/lib/components/campaign-topic-select.d.ts +32 -0
- package/src/lib/components/campaign-topic-select.js +87 -0
- package/src/lib/components/campaign-topic-select.js.map +1 -0
- package/src/lib/components/dynamic-list-rule-fields.d.ts +173 -0
- package/src/lib/components/dynamic-list-rule-fields.js +1473 -0
- package/src/lib/components/dynamic-list-rule-fields.js.map +1 -0
- package/src/lib/components/email-blocks.d.ts +111 -0
- package/src/lib/components/email-blocks.js +875 -0
- package/src/lib/components/email-blocks.js.map +1 -0
- package/src/lib/components/email-design-preview.d.ts +62 -0
- package/src/lib/components/email-design-preview.js +174 -0
- package/src/lib/components/email-design-preview.js.map +1 -0
- package/src/lib/components/email-screens-card.d.ts +42 -0
- package/src/lib/components/email-screens-card.js +277 -0
- package/src/lib/components/email-screens-card.js.map +1 -0
- package/src/lib/components/email-template-detail.d.ts +48 -0
- package/src/lib/components/email-template-detail.js +681 -0
- package/src/lib/components/email-template-detail.js.map +1 -0
- package/src/lib/components/email-topic-detail.d.ts +32 -0
- package/src/lib/components/email-topic-detail.js +293 -0
- package/src/lib/components/email-topic-detail.js.map +1 -0
- package/src/lib/components/email-topics-card.d.ts +46 -0
- package/src/lib/components/email-topics-card.js +327 -0
- package/src/lib/components/email-topics-card.js.map +1 -0
- package/src/lib/components/email-zones.d.ts +28 -0
- package/src/lib/components/email-zones.js +20 -0
- package/src/lib/components/email-zones.js.map +1 -0
- package/src/lib/components/emails-console-page.d.ts +32 -0
- package/src/lib/components/emails-console-page.js +229 -0
- package/src/lib/components/emails-console-page.js.map +1 -0
- package/src/lib/components/emails-console-sections.d.ts +36 -0
- package/src/lib/components/emails-console-sections.js +108 -0
- package/src/lib/components/emails-console-sections.js.map +1 -0
- package/src/lib/components/list-detail-card.d.ts +47 -0
- package/src/lib/components/list-detail-card.js +273 -0
- package/src/lib/components/list-detail-card.js.map +1 -0
- package/src/lib/components/list-edit-card.d.ts +11 -0
- package/src/lib/components/list-edit-card.js +287 -0
- package/src/lib/components/list-edit-card.js.map +1 -0
- package/src/lib/components/list-import-drawer.d.ts +22 -0
- package/src/lib/components/list-import-drawer.js +662 -0
- package/src/lib/components/list-import-drawer.js.map +1 -0
- package/src/lib/components/list-members-panel.d.ts +94 -0
- package/src/lib/components/list-members-panel.js +686 -0
- package/src/lib/components/list-members-panel.js.map +1 -0
- package/src/lib/components/lists-card.d.ts +28 -0
- package/src/lib/components/lists-card.js +377 -0
- package/src/lib/components/lists-card.js.map +1 -0
- package/src/lib/components/sending-domain-detail.d.ts +26 -0
- package/src/lib/components/sending-domain-detail.js +496 -0
- package/src/lib/components/sending-domain-detail.js.map +1 -0
- package/src/lib/components/sending-domains-card.d.ts +33 -0
- package/src/lib/components/sending-domains-card.js +962 -0
- package/src/lib/components/sending-domains-card.js.map +1 -0
- package/src/lib/components/sending-sender-drawer.d.ts +94 -0
- package/src/lib/components/sending-sender-drawer.js +543 -0
- package/src/lib/components/sending-sender-drawer.js.map +1 -0
- package/src/lib/components/suppressions-card.d.ts +49 -0
- package/src/lib/components/suppressions-card.js +639 -0
- package/src/lib/components/suppressions-card.js.map +1 -0
- package/src/lib/components/use-org-email-topics.d.ts +79 -0
- package/src/lib/components/use-org-email-topics.js +111 -0
- package/src/lib/components/use-org-email-topics.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +18 -0
- package/src/lib/constants/bundle-common.js +18 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/hooks/use-org-company-options.d.ts +20 -0
- package/src/lib/hooks/use-org-company-options.js +138 -0
- package/src/lib/hooks/use-org-company-options.js.map +1 -0
- package/src/lib/hooks/use-org-contact-fields.d.ts +40 -0
- package/src/lib/hooks/use-org-contact-fields.js +91 -0
- package/src/lib/hooks/use-org-contact-fields.js.map +1 -0
- package/src/lib/hooks/use-org-contact-segments.d.ts +16 -0
- package/src/lib/hooks/use-org-contact-segments.js +55 -0
- package/src/lib/hooks/use-org-contact-segments.js.map +1 -0
- package/src/lib/hooks/use-org-crm-views.d.ts +8 -0
- package/src/lib/hooks/use-org-crm-views.js +74 -0
- package/src/lib/hooks/use-org-crm-views.js.map +1 -0
- package/src/lib/hooks/use-org-lists.d.ts +8 -0
- package/src/lib/hooks/use-org-lists.js +47 -0
- package/src/lib/hooks/use-org-lists.js.map +1 -0
- package/src/lib/model/email-design-document.d.ts +52 -0
- package/src/lib/model/email-design-document.js +62 -0
- package/src/lib/model/email-design-document.js.map +1 -0
- package/src/lib/model/index.d.ts +64 -0
- package/src/lib/model/index.js +71 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/sending-domain-status.d.ts +99 -0
- package/src/lib/model/sending-domain-status.js +196 -0
- package/src/lib/model/sending-domain-status.js.map +1 -0
- package/src/lib/model/template-provenance.d.ts +113 -0
- package/src/lib/model/template-provenance.js +107 -0
- package/src/lib/model/template-provenance.js.map +1 -0
- package/src/lib/model/template-report.d.ts +158 -0
- package/src/lib/model/template-report.js +249 -0
- package/src/lib/model/template-report.js.map +1 -0
- package/src/lib/plugin.d.ts +27 -0
- package/src/lib/plugin.js +163 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server-console.d.ts +116 -0
- package/src/lib/server-console.js +422 -0
- package/src/lib/server-console.js.map +1 -0
- package/src/lib/server-email-drafts.d.ts +104 -0
- package/src/lib/server-email-drafts.js +381 -0
- package/src/lib/server-email-drafts.js.map +1 -0
- package/src/lib/server-list-gate.d.ts +183 -0
- package/src/lib/server-list-gate.js +365 -0
- package/src/lib/server-list-gate.js.map +1 -0
- package/src/lib/server-list-import.d.ts +199 -0
- package/src/lib/server-list-import.js +632 -0
- package/src/lib/server-list-import.js.map +1 -0
- package/src/lib/server-suppressions.d.ts +135 -0
- package/src/lib/server-suppressions.js +295 -0
- package/src/lib/server-suppressions.js.map +1 -0
- package/src/lib/server.d.ts +19 -0
- package/src/lib/server.js +834 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +26 -0
- package/src/lib/site.js +81 -0
- package/src/lib/site.js.map +1 -0
- package/src/lib/unsubscribe-link.d.ts +311 -0
- package/src/lib/unsubscribe-link.js +398 -0
- package/src/lib/unsubscribe-link.js.map +1 -0
- package/src/lib/utils/create-email-screen.d.ts +59 -0
- package/src/lib/utils/create-email-screen.js +59 -0
- package/src/lib/utils/create-email-screen.js.map +1 -0
- package/src/lib/utils/generate-preset-id.d.ts +19 -0
- package/src/lib/utils/generate-preset-id.js +25 -0
- package/src/lib/utils/generate-preset-id.js.map +1 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type PluginDraftCheck, type PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
|
|
18
|
+
/**
|
|
19
|
+
* AN EMAIL DESIGN ANOTHER PLUGIN ASKS FOR (AGL-2912).
|
|
20
|
+
*
|
|
21
|
+
* The draft writer this plugin registers for the `emailDesign` resource on the
|
|
22
|
+
* core's resource-drafts seam. A plugin that produces an email — a generator
|
|
23
|
+
* working from a brief, an importer bringing templates over — asks for the
|
|
24
|
+
* writer by name and gets this plugin's rules, never a copy of them:
|
|
25
|
+
*
|
|
26
|
+
* - THE DOCUMENTS are the pair `createEmailScreen` makes, built by the same
|
|
27
|
+
* function (`emailDesignDocuments`): a `kind: 'email'` screen and its first
|
|
28
|
+
* version. So the design lists on the Templates tab, opens in the besigner
|
|
29
|
+
* and is offered by the campaign composer like one a person made. The
|
|
30
|
+
* screen carries the subject and preheader a campaign built from it opens
|
|
31
|
+
* on, and the other lines worth trying beside them.
|
|
32
|
+
* - THE ROOM is the flat platform ceiling on non-page screens, met inside the
|
|
33
|
+
* transaction with the arithmetic `/api/hosts/resources` and the duplicate
|
|
34
|
+
* module use, and refused in their words.
|
|
35
|
+
* - THE ROLE is the resources route's: a member who may write the site's
|
|
36
|
+
* content.
|
|
37
|
+
* - THE CHECK renders the design through `renderRecipientEmail`, the function
|
|
38
|
+
* the send path and the composer's preview both call, so content that
|
|
39
|
+
* passes is content that would render in both.
|
|
40
|
+
*
|
|
41
|
+
* Nothing is sent, scheduled or routed. A design is inert until a campaign
|
|
42
|
+
* names it, and the writer touches the two new documents and nothing else.
|
|
43
|
+
*/
|
|
44
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
45
|
+
/** The resource name this writer is registered under. */
|
|
46
|
+
export declare const EMAIL_DESIGN_DRAFT_RESOURCE = "emailDesign";
|
|
47
|
+
/** A design's name when the caller asks for none, as the composer's create names one. */
|
|
48
|
+
export declare const EMAIL_DESIGN_DEFAULT_NAME = "Untitled email";
|
|
49
|
+
/** The longest subject or preheader stored, the send route's header cap. */
|
|
50
|
+
export declare const EMAIL_DESIGN_HEADER_MAX_CHARS = 200;
|
|
51
|
+
/** The most alternative subject lines or preheaders a design keeps. */
|
|
52
|
+
export declare const EMAIL_DESIGN_MAX_VARIANTS = 10;
|
|
53
|
+
/** The resources route's refusal at the non-page ceiling. */
|
|
54
|
+
export declare const EMAIL_DESIGN_LIMIT_REFUSAL: string;
|
|
55
|
+
/** The resources route's refusal for a member who may not write the site. */
|
|
56
|
+
export declare const EMAIL_DESIGN_ROLE_REFUSAL = "Editing requires the editor role";
|
|
57
|
+
/** One node of a design, as far as the writer reads it. */
|
|
58
|
+
export interface EmailDesignNode {
|
|
59
|
+
componentId?: unknown;
|
|
60
|
+
props?: Record<string, unknown>;
|
|
61
|
+
nodes?: unknown;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* What a caller sends as `content`: the node map rooted at the canvas root,
|
|
65
|
+
* the subject and preheader, and the alternatives to them.
|
|
66
|
+
*/
|
|
67
|
+
export interface EmailDesignDraftContent {
|
|
68
|
+
nodes: Record<string, EmailDesignNode>;
|
|
69
|
+
subject: string;
|
|
70
|
+
preheader: string;
|
|
71
|
+
subjectVariants: string[];
|
|
72
|
+
preheaderVariants: string[];
|
|
73
|
+
}
|
|
74
|
+
export type EmailDesignContentRead = {
|
|
75
|
+
ok: true;
|
|
76
|
+
value: EmailDesignDraftContent;
|
|
77
|
+
reachable: Set<string>;
|
|
78
|
+
} | {
|
|
79
|
+
ok: false;
|
|
80
|
+
problems: string[];
|
|
81
|
+
};
|
|
82
|
+
/** The content as the writer stores it, or every problem that stops it. */
|
|
83
|
+
export declare function readEmailDesignContent(content: Readonly<Record<string, unknown>>): EmailDesignContentRead;
|
|
84
|
+
/**
|
|
85
|
+
* Whether content renders as an email: well-formed, every link one an inbox
|
|
86
|
+
* can open, and a message once rendered through the send path's own renderer.
|
|
87
|
+
*/
|
|
88
|
+
export declare function checkEmailDesignContent(content: Readonly<Record<string, unknown>>, context: {
|
|
89
|
+
hostId: string;
|
|
90
|
+
}): PluginDraftCheck;
|
|
91
|
+
export interface EmailDesignDraftWriterDeps {
|
|
92
|
+
/** The Admin SDK handle; specs hand in a double. */
|
|
93
|
+
firestore?: () => Firestore;
|
|
94
|
+
/** Mints the first version's id. */
|
|
95
|
+
mintId?: () => string;
|
|
96
|
+
}
|
|
97
|
+
export declare function createEmailDesignDraftWriter(deps?: EmailDesignDraftWriterDeps): PluginResourceDraftWriter;
|
|
98
|
+
export declare const emailDesignDraftWriter: PluginResourceDraftWriter;
|
|
99
|
+
/**
|
|
100
|
+
* Registers the writer; the console surface calls it, since only the console
|
|
101
|
+
* runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.
|
|
102
|
+
*/
|
|
103
|
+
export declare function registerEmailDesignDraftWriter(): void;
|
|
104
|
+
export {};
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
|
|
18
|
+
import { uniqueDuplicateName } from "@aglyn/aglyn/app-utils/duplicate-resource";
|
|
19
|
+
import { nameSearchKey } from "@aglyn/aglyn/app-utils/name-search";
|
|
20
|
+
import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
|
|
21
|
+
import { NON_PAGE_SCREEN_MAX_PER_HOST, nonPageScreenIds, SCREEN_KIND_EMAIL } from "@aglyn/aglyn/app-utils/screen-route";
|
|
22
|
+
import { encodeStoredNodes } from "@aglyn/aglyn/app-utils/stored-nodes";
|
|
23
|
+
import { CANVAS_ROOT_ELEMENT_ID } from "@aglyn/aglyn/foundation/constants/canvas";
|
|
24
|
+
import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
|
|
25
|
+
import { hasSafeLinkScheme } from "@aglyn/shared-util-http/safe-url-scheme";
|
|
26
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
27
|
+
import { emailPlainTextState, renderRecipientEmail } from "@aglyn/aglyn/app-utils/recipient-email-render";
|
|
28
|
+
import { emailDesignDocuments } from "./model/email-design-document.js";
|
|
29
|
+
/** The resource name this writer is registered under. */ export const EMAIL_DESIGN_DRAFT_RESOURCE = 'emailDesign';
|
|
30
|
+
/** The id `plugins.config.json` registers this plugin under. */ const EMAIL_PLUGIN_ID = 'email';
|
|
31
|
+
/** A design's name when the caller asks for none, as the composer's create names one. */ export const EMAIL_DESIGN_DEFAULT_NAME = 'Untitled email';
|
|
32
|
+
/** The longest subject or preheader stored, the send route's header cap. */ export const EMAIL_DESIGN_HEADER_MAX_CHARS = 200;
|
|
33
|
+
/** The most alternative subject lines or preheaders a design keeps. */ export const EMAIL_DESIGN_MAX_VARIANTS = 10;
|
|
34
|
+
/** The resources route's refusal at the non-page ceiling. */ export const EMAIL_DESIGN_LIMIT_REFUSAL = 'This site is at its limit of ' + `${NON_PAGE_SCREEN_MAX_PER_HOST} email and template screens — ` + 'delete some to make room';
|
|
35
|
+
/** The resources route's refusal for a member who may not write the site. */ export const EMAIL_DESIGN_ROLE_REFUSAL = 'Editing requires the editor role';
|
|
36
|
+
/*
|
|
37
|
+
* What the check renders against. No mail is sent and nothing is fetched: the
|
|
38
|
+
* origin makes the renderer keep images and links it would drop without one,
|
|
39
|
+
* and the recipient fills the merge tokens a real send would fill.
|
|
40
|
+
*/ const CHECK_SITE_BASE = 'https://example.com';
|
|
41
|
+
const CHECK_RECIPIENT = {
|
|
42
|
+
email: 'reader@example.com',
|
|
43
|
+
name: 'Sample Reader'
|
|
44
|
+
};
|
|
45
|
+
function isRecord(value) {
|
|
46
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
/** A header value on one line: controls and runs of space folded to one space. */ function headerLine(value) {
|
|
49
|
+
return String(value != null ? value : '').replace(RegExp("[\\u0000-\\u001F\\u007F-\\u009F\\s]+", "gu"), ' ').trim();
|
|
50
|
+
}
|
|
51
|
+
/** Alternatives as the design keeps them: on one line, unique, non-empty, bounded. */ function variantsOf(value) {
|
|
52
|
+
if (!Array.isArray(value)) return [];
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const lines = [];
|
|
55
|
+
for (const raw of value){
|
|
56
|
+
const line = headerLine(raw);
|
|
57
|
+
const key = line.toLowerCase();
|
|
58
|
+
if (!line || seen.has(key)) continue;
|
|
59
|
+
seen.add(key);
|
|
60
|
+
lines.push(line);
|
|
61
|
+
}
|
|
62
|
+
return lines.slice(0, EMAIL_DESIGN_MAX_VARIANTS);
|
|
63
|
+
}
|
|
64
|
+
/** The ids reachable from the canvas root, or the structural problem that stops the walk. */ function reachableIds(nodes) {
|
|
65
|
+
const ids = new Set();
|
|
66
|
+
const problems = new Set();
|
|
67
|
+
const stack = [
|
|
68
|
+
CANVAS_ROOT_ELEMENT_ID
|
|
69
|
+
];
|
|
70
|
+
while(stack.length){
|
|
71
|
+
const id = stack.pop();
|
|
72
|
+
if (ids.has(id)) {
|
|
73
|
+
problems.add('A block is placed in the design more than once');
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const node = nodes[id];
|
|
77
|
+
if (!isRecord(node)) {
|
|
78
|
+
problems.add('The design lists a block it does not hold');
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
ids.add(id);
|
|
82
|
+
for (const child of Array.isArray(node.nodes) ? node.nodes : []){
|
|
83
|
+
if (typeof child === 'string') stack.push(child);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
ids,
|
|
88
|
+
problems: [
|
|
89
|
+
...problems
|
|
90
|
+
]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** The content as the writer stores it, or every problem that stops it. */ export function readEmailDesignContent(content) {
|
|
94
|
+
const nodes = content['nodes'];
|
|
95
|
+
if (!isRecord(nodes) || !Object.keys(nodes).length) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
problems: [
|
|
99
|
+
'The design has no blocks'
|
|
100
|
+
]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const map = nodes;
|
|
104
|
+
if (!isRecord(map[CANVAS_ROOT_ELEMENT_ID])) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
problems: [
|
|
108
|
+
'The design has no root to render from'
|
|
109
|
+
]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const { ids, problems } = reachableIds(map);
|
|
113
|
+
const stranded = Object.keys(map).filter((id)=>!ids.has(id)).length;
|
|
114
|
+
if (stranded) {
|
|
115
|
+
problems.push(`${stranded} ${stranded === 1 ? 'block is' : 'blocks are'} not placed in the design, so ${stranded === 1 ? 'it' : 'they'} would not be sent`);
|
|
116
|
+
}
|
|
117
|
+
const value = {
|
|
118
|
+
nodes: map,
|
|
119
|
+
subject: headerLine(content['subject']),
|
|
120
|
+
preheader: headerLine(content['preheader']),
|
|
121
|
+
subjectVariants: variantsOf(content['subjectVariants']),
|
|
122
|
+
preheaderVariants: variantsOf(content['preheaderVariants'])
|
|
123
|
+
};
|
|
124
|
+
const long = (lines)=>lines.some((line)=>line.length > EMAIL_DESIGN_HEADER_MAX_CHARS);
|
|
125
|
+
if (long([
|
|
126
|
+
value.subject,
|
|
127
|
+
...value.subjectVariants
|
|
128
|
+
])) {
|
|
129
|
+
problems.push(`A subject line is longer than ${EMAIL_DESIGN_HEADER_MAX_CHARS} characters`);
|
|
130
|
+
}
|
|
131
|
+
if (long([
|
|
132
|
+
value.preheader,
|
|
133
|
+
...value.preheaderVariants
|
|
134
|
+
])) {
|
|
135
|
+
problems.push(`A preheader is longer than ${EMAIL_DESIGN_HEADER_MAX_CHARS} characters`);
|
|
136
|
+
}
|
|
137
|
+
return problems.length ? {
|
|
138
|
+
ok: false,
|
|
139
|
+
problems
|
|
140
|
+
} : {
|
|
141
|
+
ok: true,
|
|
142
|
+
value,
|
|
143
|
+
reachable: ids
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Why a link on a block would not open from an inbox, or `null`. */ function linkProblem(label, raw, required) {
|
|
147
|
+
const written = String(raw != null ? raw : '').trim();
|
|
148
|
+
if (/\{\{\s*contact\./.test(written)) {
|
|
149
|
+
return `${label} puts the reader's own details in its link, which is never filled`;
|
|
150
|
+
}
|
|
151
|
+
const href = written.replace(/\{\{\s*site\.url\s*\}\}/g, CHECK_SITE_BASE).replace(/\{\{\s*unsubscribeUrl\s*\}\}/g, `${CHECK_SITE_BASE}/preferences`);
|
|
152
|
+
if (!href || href === '#') return required ? `${label} links nowhere` : null;
|
|
153
|
+
if (href.startsWith('/') && !href.startsWith('//')) {
|
|
154
|
+
return `${label} links to a path with no site in front of it, which an inbox cannot open`;
|
|
155
|
+
}
|
|
156
|
+
return hasSafeLinkScheme(href) ? null : `${label} links to something that is not a web address`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Whether content renders as an email: well-formed, every link one an inbox
|
|
160
|
+
* can open, and a message once rendered through the send path's own renderer.
|
|
161
|
+
*/ export function checkEmailDesignContent(content, context) {
|
|
162
|
+
const read = readEmailDesignContent(content);
|
|
163
|
+
if (read.ok === false) return read;
|
|
164
|
+
const { value, reachable } = read;
|
|
165
|
+
const problems = [];
|
|
166
|
+
for (const id of reachable){
|
|
167
|
+
const node = value.nodes[id];
|
|
168
|
+
if (node.componentId === 'emailButton') {
|
|
169
|
+
var _node_props;
|
|
170
|
+
const problem = linkProblem('A button', (_node_props = node.props) == null ? void 0 : _node_props['href'], true);
|
|
171
|
+
if (problem) problems.push(problem);
|
|
172
|
+
}
|
|
173
|
+
if (node.componentId === 'emailImage') {
|
|
174
|
+
var _node_props1;
|
|
175
|
+
const problem = linkProblem('An image', (_node_props1 = node.props) == null ? void 0 : _node_props1['href'], false);
|
|
176
|
+
if (problem) problems.push(problem);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const rendered = renderRecipientEmail({
|
|
180
|
+
subject: value.subject,
|
|
181
|
+
preheader: value.preheader,
|
|
182
|
+
content: {
|
|
183
|
+
mode: 'design',
|
|
184
|
+
template: {
|
|
185
|
+
nodes: value.nodes,
|
|
186
|
+
subject: value.subject,
|
|
187
|
+
preheader: value.preheader
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
recipient: CHECK_RECIPIENT,
|
|
191
|
+
siteBase: CHECK_SITE_BASE,
|
|
192
|
+
hostId: context.hostId
|
|
193
|
+
});
|
|
194
|
+
if (!rendered.messageText.trim()) {
|
|
195
|
+
problems.push('The design renders no message: it needs at least one block of text');
|
|
196
|
+
}
|
|
197
|
+
if (problems.length) return {
|
|
198
|
+
ok: false,
|
|
199
|
+
problems: [
|
|
200
|
+
...new Set(problems)
|
|
201
|
+
]
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
ok: true,
|
|
205
|
+
facts: {
|
|
206
|
+
htmlBytes: new TextEncoder().encode(rendered.html).length,
|
|
207
|
+
messageText: rendered.messageText,
|
|
208
|
+
blocks: reachable.size,
|
|
209
|
+
// No part is written by hand, so the text a campaign sends is the design's own.
|
|
210
|
+
plainText: emailPlainTextState({})
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async function readScreenRows(hostRef, read) {
|
|
215
|
+
const snapshot = await read(hostRef.collection('screens').select('displayName', 'kind', 'deletedAt'));
|
|
216
|
+
return snapshot.docs.map((doc)=>{
|
|
217
|
+
var _doc_get;
|
|
218
|
+
return {
|
|
219
|
+
id: doc.id,
|
|
220
|
+
kind: doc.get('kind'),
|
|
221
|
+
deletedAt: doc.get('deletedAt'),
|
|
222
|
+
name: String((_doc_get = doc.get('displayName')) != null ? _doc_get : '')
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function roleRefusal(host, uid) {
|
|
227
|
+
var _host_get;
|
|
228
|
+
const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
|
|
229
|
+
return hostRoleCanWrite(role) ? null : {
|
|
230
|
+
status: 403,
|
|
231
|
+
error: EMAIL_DESIGN_ROLE_REFUSAL
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function roomRefusal(rows, host) {
|
|
235
|
+
const used = nonPageScreenIds(rows, host.get('screens')).size;
|
|
236
|
+
return used >= NON_PAGE_SCREEN_MAX_PER_HOST ? {
|
|
237
|
+
status: 403,
|
|
238
|
+
error: EMAIL_DESIGN_LIMIT_REFUSAL
|
|
239
|
+
} : null;
|
|
240
|
+
}
|
|
241
|
+
function strings(value) {
|
|
242
|
+
return Array.isArray(value) ? value.filter((entry)=>typeof entry === 'string') : [];
|
|
243
|
+
}
|
|
244
|
+
function recordOf(screen) {
|
|
245
|
+
var _screen_get, _screen_get1, _screen_get2;
|
|
246
|
+
const versionId = screen.get('versionId');
|
|
247
|
+
return {
|
|
248
|
+
id: screen.id,
|
|
249
|
+
name: String((_screen_get = screen.get('displayName')) != null ? _screen_get : ''),
|
|
250
|
+
versionId: typeof versionId === 'string' && versionId ? versionId : null,
|
|
251
|
+
facts: {
|
|
252
|
+
subject: String((_screen_get1 = screen.get('emailSubject')) != null ? _screen_get1 : ''),
|
|
253
|
+
preheader: String((_screen_get2 = screen.get('emailPreheader')) != null ? _screen_get2 : ''),
|
|
254
|
+
subjectVariants: strings(screen.get('emailSubjectVariants')),
|
|
255
|
+
preheaderVariants: strings(screen.get('emailPreheaderVariants'))
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/** The subject, preheader and alternatives as the screen document stores them. */ function copyFields(value) {
|
|
260
|
+
return _extends({}, value.subject ? {
|
|
261
|
+
emailSubject: value.subject
|
|
262
|
+
} : {}, value.preheader ? {
|
|
263
|
+
emailPreheader: value.preheader
|
|
264
|
+
} : {}, value.subjectVariants.length ? {
|
|
265
|
+
emailSubjectVariants: value.subjectVariants
|
|
266
|
+
} : {}, value.preheaderVariants.length ? {
|
|
267
|
+
emailPreheaderVariants: value.preheaderVariants
|
|
268
|
+
} : {});
|
|
269
|
+
}
|
|
270
|
+
export function createEmailDesignDraftWriter(deps = {}) {
|
|
271
|
+
var _deps_firestore, _deps_mintId;
|
|
272
|
+
const firestore = (_deps_firestore = deps.firestore) != null ? _deps_firestore : ()=>firebaseAdmin.app().firestore();
|
|
273
|
+
const mintId = (_deps_mintId = deps.mintId) != null ? _deps_mintId : createResourceUid;
|
|
274
|
+
return {
|
|
275
|
+
refusal: async (context)=>{
|
|
276
|
+
var _roleRefusal;
|
|
277
|
+
const hostRef = firestore().collection('hosts').doc(context.hostId);
|
|
278
|
+
const host = await hostRef.get();
|
|
279
|
+
if (!host.exists) return {
|
|
280
|
+
status: 404,
|
|
281
|
+
error: 'Unknown site'
|
|
282
|
+
};
|
|
283
|
+
return (_roleRefusal = roleRefusal(host, context.uid)) != null ? _roleRefusal : roomRefusal(await readScreenRows(hostRef, (query)=>query.get()), host);
|
|
284
|
+
},
|
|
285
|
+
check: checkEmailDesignContent,
|
|
286
|
+
read: async ({ hostId, id })=>{
|
|
287
|
+
const screen = await firestore().collection('hosts').doc(hostId).collection('screens').doc(id).get();
|
|
288
|
+
return screen.exists && screen.get('kind') === SCREEN_KIND_EMAIL ? recordOf(screen) : null;
|
|
289
|
+
},
|
|
290
|
+
write: async (request)=>{
|
|
291
|
+
const read = readEmailDesignContent(request.content);
|
|
292
|
+
if (read.ok === false) return {
|
|
293
|
+
ok: false,
|
|
294
|
+
status: 400,
|
|
295
|
+
error: read.problems[0]
|
|
296
|
+
};
|
|
297
|
+
const packed = encodeStoredNodes(read.value.nodes);
|
|
298
|
+
if (!packed) return {
|
|
299
|
+
ok: false,
|
|
300
|
+
status: 400,
|
|
301
|
+
error: 'The design has no blocks'
|
|
302
|
+
};
|
|
303
|
+
const db = firestore();
|
|
304
|
+
const hostRef = db.collection('hosts').doc(request.hostId);
|
|
305
|
+
const screenRef = hostRef.collection('screens').doc(request.id);
|
|
306
|
+
const versionId = mintId();
|
|
307
|
+
return db.runTransaction(async (tx)=>{
|
|
308
|
+
var _roleRefusal;
|
|
309
|
+
// Every read before any write, which Firestore requires.
|
|
310
|
+
const [host, existing, rows] = await Promise.all([
|
|
311
|
+
tx.get(hostRef),
|
|
312
|
+
tx.get(screenRef),
|
|
313
|
+
readScreenRows(hostRef, (query)=>tx.get(query))
|
|
314
|
+
]);
|
|
315
|
+
if (!host.exists) return {
|
|
316
|
+
ok: false,
|
|
317
|
+
status: 404,
|
|
318
|
+
error: 'Unknown site'
|
|
319
|
+
};
|
|
320
|
+
if (existing.exists) {
|
|
321
|
+
return existing.get('kind') === SCREEN_KIND_EMAIL ? _extends({
|
|
322
|
+
ok: true,
|
|
323
|
+
replayed: true
|
|
324
|
+
}, recordOf(existing)) : {
|
|
325
|
+
ok: false,
|
|
326
|
+
status: 409,
|
|
327
|
+
error: 'That id already names a screen that is not an email design'
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const refusal = (_roleRefusal = roleRefusal(host, request.uid)) != null ? _roleRefusal : roomRefusal(rows, host);
|
|
331
|
+
if (refusal) return _extends({
|
|
332
|
+
ok: false
|
|
333
|
+
}, refusal);
|
|
334
|
+
const name = uniqueDuplicateName(headerLine(request.name) || EMAIL_DESIGN_DEFAULT_NAME, rows.filter((row)=>row.deletedAt == null).map((row)=>row.name));
|
|
335
|
+
const { screen, version } = emailDesignDocuments({
|
|
336
|
+
screenId: request.id,
|
|
337
|
+
versionId,
|
|
338
|
+
displayName: name,
|
|
339
|
+
nodes: read.value.nodes
|
|
340
|
+
});
|
|
341
|
+
const stamps = {
|
|
342
|
+
createdAt: request.now,
|
|
343
|
+
updatedAt: request.now,
|
|
344
|
+
createdBy: request.uid
|
|
345
|
+
};
|
|
346
|
+
const copy = copyFields(read.value);
|
|
347
|
+
tx.create(screenRef, _extends({}, screen, {
|
|
348
|
+
nameLower: nameSearchKey(name)
|
|
349
|
+
}, copy, stamps));
|
|
350
|
+
tx.create(screenRef.collection('versions').doc(versionId), _extends({}, version, {
|
|
351
|
+
nodes: Buffer.from(packed),
|
|
352
|
+
hostId: request.hostId
|
|
353
|
+
}, stamps));
|
|
354
|
+
return {
|
|
355
|
+
ok: true,
|
|
356
|
+
replayed: false,
|
|
357
|
+
id: request.id,
|
|
358
|
+
name,
|
|
359
|
+
versionId,
|
|
360
|
+
facts: {
|
|
361
|
+
subject: read.value.subject,
|
|
362
|
+
preheader: read.value.preheader,
|
|
363
|
+
subjectVariants: read.value.subjectVariants,
|
|
364
|
+
preheaderVariants: read.value.preheaderVariants
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
export const emailDesignDraftWriter = createEmailDesignDraftWriter();
|
|
372
|
+
/**
|
|
373
|
+
* Registers the writer; the console surface calls it, since only the console
|
|
374
|
+
* runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.
|
|
375
|
+
*/ export function registerEmailDesignDraftWriter() {
|
|
376
|
+
registerPluginResourceDraftWriter(EMAIL_DESIGN_DRAFT_RESOURCE, emailDesignDraftWriter, {
|
|
377
|
+
pluginId: EMAIL_PLUGIN_ID
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
//# sourceMappingURL=server-email-drafts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/email/src/lib/server-email-drafts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { uniqueDuplicateName } from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport { nameSearchKey } from '@aglyn/aglyn/app-utils/name-search'\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport {\n NON_PAGE_SCREEN_MAX_PER_HOST,\n nonPageScreenIds,\n SCREEN_KIND_EMAIL,\n type BillableScreenSource,\n} from '@aglyn/aglyn/app-utils/screen-route'\nimport { encodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { hasSafeLinkScheme } from '@aglyn/shared-util-http/safe-url-scheme'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n emailPlainTextState,\n renderRecipientEmail,\n} from '@aglyn/aglyn/app-utils/recipient-email-render'\nimport { emailDesignDocuments } from './model/email-design-document'\n\n/**\n * AN EMAIL DESIGN ANOTHER PLUGIN ASKS FOR (AGL-2912).\n *\n * The draft writer this plugin registers for the `emailDesign` resource on the\n * core's resource-drafts seam. A plugin that produces an email — a generator\n * working from a brief, an importer bringing templates over — asks for the\n * writer by name and gets this plugin's rules, never a copy of them:\n *\n * - THE DOCUMENTS are the pair `createEmailScreen` makes, built by the same\n * function (`emailDesignDocuments`): a `kind: 'email'` screen and its first\n * version. So the design lists on the Templates tab, opens in the besigner\n * and is offered by the campaign composer like one a person made. The\n * screen carries the subject and preheader a campaign built from it opens\n * on, and the other lines worth trying beside them.\n * - THE ROOM is the flat platform ceiling on non-page screens, met inside the\n * transaction with the arithmetic `/api/hosts/resources` and the duplicate\n * module use, and refused in their words.\n * - THE ROLE is the resources route's: a member who may write the site's\n * content.\n * - THE CHECK renders the design through `renderRecipientEmail`, the function\n * the send path and the composer's preview both call, so content that\n * passes is content that would render in both.\n *\n * Nothing is sent, scheduled or routed. A design is inert until a campaign\n * names it, and the writer touches the two new documents and nothing else.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const EMAIL_DESIGN_DRAFT_RESOURCE = 'emailDesign'\n\n/** The id `plugins.config.json` registers this plugin under. */\nconst EMAIL_PLUGIN_ID = 'email'\n\n/** A design's name when the caller asks for none, as the composer's create names one. */\nexport const EMAIL_DESIGN_DEFAULT_NAME = 'Untitled email'\n\n/** The longest subject or preheader stored, the send route's header cap. */\nexport const EMAIL_DESIGN_HEADER_MAX_CHARS = 200\n\n/** The most alternative subject lines or preheaders a design keeps. */\nexport const EMAIL_DESIGN_MAX_VARIANTS = 10\n\n/** The resources route's refusal at the non-page ceiling. */\nexport const EMAIL_DESIGN_LIMIT_REFUSAL =\n 'This site is at its limit of ' +\n `${NON_PAGE_SCREEN_MAX_PER_HOST} email and template screens — ` +\n 'delete some to make room'\n\n/** The resources route's refusal for a member who may not write the site. */\nexport const EMAIL_DESIGN_ROLE_REFUSAL = 'Editing requires the editor role'\n\n/*\n * What the check renders against. No mail is sent and nothing is fetched: the\n * origin makes the renderer keep images and links it would drop without one,\n * and the recipient fills the merge tokens a real send would fill.\n */\nconst CHECK_SITE_BASE = 'https://example.com'\nconst CHECK_RECIPIENT = { email: 'reader@example.com', name: 'Sample Reader' }\n\n/** One node of a design, as far as the writer reads it. */\nexport interface EmailDesignNode {\n componentId?: unknown\n props?: Record<string, unknown>\n nodes?: unknown\n}\n\n/**\n * What a caller sends as `content`: the node map rooted at the canvas root,\n * the subject and preheader, and the alternatives to them.\n */\nexport interface EmailDesignDraftContent {\n nodes: Record<string, EmailDesignNode>\n subject: string\n preheader: string\n subjectVariants: string[]\n preheaderVariants: string[]\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** A header value on one line: controls and runs of space folded to one space. */\nfunction headerLine(value: unknown): string {\n return String(value ?? '').replace(/[\\p{Cc}\\s]+/gu, ' ').trim()\n}\n\n/** Alternatives as the design keeps them: on one line, unique, non-empty, bounded. */\nfunction variantsOf(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n const seen = new Set<string>()\n const lines: string[] = []\n for (const raw of value) {\n const line = headerLine(raw)\n const key = line.toLowerCase()\n if (!line || seen.has(key)) continue\n seen.add(key)\n lines.push(line)\n }\n return lines.slice(0, EMAIL_DESIGN_MAX_VARIANTS)\n}\n\n/** The ids reachable from the canvas root, or the structural problem that stops the walk. */\nfunction reachableIds(nodes: Record<string, EmailDesignNode>): { ids: Set<string>; problems: string[] } {\n const ids = new Set<string>()\n const problems = new Set<string>()\n const stack = [CANVAS_ROOT_ELEMENT_ID]\n while (stack.length) {\n const id = stack.pop() as string\n if (ids.has(id)) {\n problems.add('A block is placed in the design more than once')\n continue\n }\n const node = nodes[id]\n if (!isRecord(node)) {\n problems.add('The design lists a block it does not hold')\n continue\n }\n ids.add(id)\n for (const child of Array.isArray(node.nodes) ? node.nodes : []) {\n if (typeof child === 'string') stack.push(child)\n }\n }\n return { ids, problems: [...problems] }\n}\n\nexport type EmailDesignContentRead =\n | { ok: true; value: EmailDesignDraftContent; reachable: Set<string> }\n | { ok: false; problems: string[] }\n\n/** The content as the writer stores it, or every problem that stops it. */\nexport function readEmailDesignContent(content: Readonly<Record<string, unknown>>): EmailDesignContentRead {\n const nodes = content['nodes']\n if (!isRecord(nodes) || !Object.keys(nodes).length) {\n return { ok: false, problems: ['The design has no blocks'] }\n }\n const map = nodes as Record<string, EmailDesignNode>\n if (!isRecord(map[CANVAS_ROOT_ELEMENT_ID])) {\n return { ok: false, problems: ['The design has no root to render from'] }\n }\n const { ids, problems } = reachableIds(map)\n const stranded = Object.keys(map).filter((id) => !ids.has(id)).length\n if (stranded) {\n problems.push(\n `${stranded} ${stranded === 1 ? 'block is' : 'blocks are'} not placed in the design, so ${\n stranded === 1 ? 'it' : 'they'\n } would not be sent`,\n )\n }\n const value: EmailDesignDraftContent = {\n nodes: map,\n subject: headerLine(content['subject']),\n preheader: headerLine(content['preheader']),\n subjectVariants: variantsOf(content['subjectVariants']),\n preheaderVariants: variantsOf(content['preheaderVariants']),\n }\n const long = (lines: string[]) => lines.some((line) => line.length > EMAIL_DESIGN_HEADER_MAX_CHARS)\n if (long([value.subject, ...value.subjectVariants])) {\n problems.push(`A subject line is longer than ${EMAIL_DESIGN_HEADER_MAX_CHARS} characters`)\n }\n if (long([value.preheader, ...value.preheaderVariants])) {\n problems.push(`A preheader is longer than ${EMAIL_DESIGN_HEADER_MAX_CHARS} characters`)\n }\n return problems.length ? { ok: false, problems } : { ok: true, value, reachable: ids }\n}\n\n/** Why a link on a block would not open from an inbox, or `null`. */\nfunction linkProblem(label: string, raw: unknown, required: boolean): string | null {\n const written = String(raw ?? '').trim()\n if (/\\{\\{\\s*contact\\./.test(written)) {\n return `${label} puts the reader's own details in its link, which is never filled`\n }\n const href = written\n .replace(/\\{\\{\\s*site\\.url\\s*\\}\\}/g, CHECK_SITE_BASE)\n .replace(/\\{\\{\\s*unsubscribeUrl\\s*\\}\\}/g, `${CHECK_SITE_BASE}/preferences`)\n if (!href || href === '#') return required ? `${label} links nowhere` : null\n if (href.startsWith('/') && !href.startsWith('//')) {\n return `${label} links to a path with no site in front of it, which an inbox cannot open`\n }\n return hasSafeLinkScheme(href) ? null : `${label} links to something that is not a web address`\n}\n\n/**\n * Whether content renders as an email: well-formed, every link one an inbox\n * can open, and a message once rendered through the send path's own renderer.\n */\nexport function checkEmailDesignContent(\n content: Readonly<Record<string, unknown>>,\n context: { hostId: string },\n): PluginDraftCheck {\n const read = readEmailDesignContent(content)\n if (read.ok === false) return read\n const { value, reachable } = read\n const problems: string[] = []\n for (const id of reachable) {\n const node = value.nodes[id]\n if (node.componentId === 'emailButton') {\n const problem = linkProblem('A button', node.props?.['href'], true)\n if (problem) problems.push(problem)\n }\n if (node.componentId === 'emailImage') {\n const problem = linkProblem('An image', node.props?.['href'], false)\n if (problem) problems.push(problem)\n }\n }\n const rendered = renderRecipientEmail({\n subject: value.subject,\n preheader: value.preheader,\n content: {\n mode: 'design',\n template: { nodes: value.nodes, subject: value.subject, preheader: value.preheader },\n },\n recipient: CHECK_RECIPIENT,\n siteBase: CHECK_SITE_BASE,\n hostId: context.hostId,\n })\n if (!rendered.messageText.trim()) {\n problems.push('The design renders no message: it needs at least one block of text')\n }\n if (problems.length) return { ok: false, problems: [...new Set(problems)] }\n return {\n ok: true,\n facts: {\n htmlBytes: new TextEncoder().encode(rendered.html).length,\n messageText: rendered.messageText,\n blocks: reachable.size,\n // No part is written by hand, so the text a campaign sends is the design's own.\n plainText: emailPlainTextState({}),\n },\n }\n}\n\ninterface ScreenRow extends BillableScreenSource {\n name: string\n}\n\nasync function readScreenRows(\n hostRef: FirebaseFirestore.DocumentReference,\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>,\n): Promise<ScreenRow[]> {\n const snapshot = await read(hostRef.collection('screens').select('displayName', 'kind', 'deletedAt'))\n return snapshot.docs.map((doc) => ({\n id: doc.id,\n kind: doc.get('kind'),\n deletedAt: doc.get('deletedAt'),\n name: String(doc.get('displayName') ?? ''),\n }))\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: EMAIL_DESIGN_ROLE_REFUSAL }\n}\n\nfunction roomRefusal(rows: ScreenRow[], host: FirebaseFirestore.DocumentSnapshot): PluginDraftRefusal | null {\n const used = nonPageScreenIds(rows, host.get('screens') as never).size\n return used >= NON_PAGE_SCREEN_MAX_PER_HOST ? { status: 403, error: EMAIL_DESIGN_LIMIT_REFUSAL } : null\n}\n\nfunction strings(value: unknown): string[] {\n return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n}\n\nfunction recordOf(screen: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const versionId = screen.get('versionId')\n return {\n id: screen.id,\n name: String(screen.get('displayName') ?? ''),\n versionId: typeof versionId === 'string' && versionId ? versionId : null,\n facts: {\n subject: String(screen.get('emailSubject') ?? ''),\n preheader: String(screen.get('emailPreheader') ?? ''),\n subjectVariants: strings(screen.get('emailSubjectVariants')),\n preheaderVariants: strings(screen.get('emailPreheaderVariants')),\n },\n }\n}\n\n/** The subject, preheader and alternatives as the screen document stores them. */\nfunction copyFields(value: EmailDesignDraftContent): Record<string, unknown> {\n return {\n ...(value.subject ? { emailSubject: value.subject } : {}),\n ...(value.preheader ? { emailPreheader: value.preheader } : {}),\n ...(value.subjectVariants.length ? { emailSubjectVariants: value.subjectVariants } : {}),\n ...(value.preheaderVariants.length ? { emailPreheaderVariants: value.preheaderVariants } : {}),\n }\n}\n\nexport interface EmailDesignDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n /** Mints the first version's id. */\n mintId?: () => string\n}\n\nexport function createEmailDesignDraftWriter(\n deps: EmailDesignDraftWriterDeps = {},\n): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n const mintId = deps.mintId ?? createResourceUid\n return {\n refusal: async (context) => {\n const hostRef = firestore().collection('hosts').doc(context.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return (\n roleRefusal(host, context.uid) ??\n roomRefusal(await readScreenRows(hostRef, (query) => query.get()), host)\n )\n },\n\n check: checkEmailDesignContent,\n\n read: async ({ hostId, id }) => {\n const screen = await firestore().collection('hosts').doc(hostId).collection('screens').doc(id).get()\n return screen.exists && screen.get('kind') === SCREEN_KIND_EMAIL ? recordOf(screen) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readEmailDesignContent(request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const packed = encodeStoredNodes(read.value.nodes)\n if (!packed) return { ok: false, status: 400, error: 'The design has no blocks' }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const screenRef = hostRef.collection('screens').doc(request.id)\n const versionId = mintId()\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing, rows] = await Promise.all([\n tx.get(hostRef),\n tx.get(screenRef),\n readScreenRows(hostRef, (query) => tx.get(query)),\n ])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) {\n return existing.get('kind') === SCREEN_KIND_EMAIL\n ? { ok: true, replayed: true, ...recordOf(existing) }\n : { ok: false, status: 409, error: 'That id already names a screen that is not an email design' }\n }\n const refusal = roleRefusal(host, request.uid) ?? roomRefusal(rows, host)\n if (refusal) return { ok: false, ...refusal }\n const name = uniqueDuplicateName(\n headerLine(request.name) || EMAIL_DESIGN_DEFAULT_NAME,\n rows.filter((row) => row.deletedAt == null).map((row) => row.name),\n )\n const { screen, version } = emailDesignDocuments({\n screenId: request.id,\n versionId,\n displayName: name,\n nodes: read.value.nodes,\n })\n const stamps = { createdAt: request.now, updatedAt: request.now, createdBy: request.uid }\n const copy = copyFields(read.value)\n tx.create(screenRef, { ...screen, nameLower: nameSearchKey(name), ...copy, ...stamps })\n tx.create(screenRef.collection('versions').doc(versionId), {\n ...version,\n nodes: Buffer.from(packed),\n hostId: request.hostId,\n ...stamps,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name,\n versionId,\n facts: {\n subject: read.value.subject,\n preheader: read.value.preheader,\n subjectVariants: read.value.subjectVariants,\n preheaderVariants: read.value.preheaderVariants,\n },\n }\n })\n },\n }\n}\n\nexport const emailDesignDraftWriter = createEmailDesignDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerEmailDesignDraftWriter(): void {\n registerPluginResourceDraftWriter(EMAIL_DESIGN_DRAFT_RESOURCE, emailDesignDraftWriter, {\n pluginId: EMAIL_PLUGIN_ID,\n })\n}\n"],"names":["createResourceUid","uniqueDuplicateName","nameSearchKey","hostRoleCanWrite","NON_PAGE_SCREEN_MAX_PER_HOST","nonPageScreenIds","SCREEN_KIND_EMAIL","encodeStoredNodes","CANVAS_ROOT_ELEMENT_ID","registerPluginResourceDraftWriter","hasSafeLinkScheme","firebaseAdmin","emailPlainTextState","renderRecipientEmail","emailDesignDocuments","EMAIL_DESIGN_DRAFT_RESOURCE","EMAIL_PLUGIN_ID","EMAIL_DESIGN_DEFAULT_NAME","EMAIL_DESIGN_HEADER_MAX_CHARS","EMAIL_DESIGN_MAX_VARIANTS","EMAIL_DESIGN_LIMIT_REFUSAL","EMAIL_DESIGN_ROLE_REFUSAL","CHECK_SITE_BASE","CHECK_RECIPIENT","email","name","isRecord","value","Array","isArray","headerLine","String","replace","trim","variantsOf","seen","Set","lines","raw","line","key","toLowerCase","has","add","push","slice","reachableIds","nodes","ids","problems","stack","length","id","pop","node","child","readEmailDesignContent","content","Object","keys","ok","map","stranded","filter","subject","preheader","subjectVariants","preheaderVariants","long","some","reachable","linkProblem","label","required","written","test","href","startsWith","checkEmailDesignContent","context","read","componentId","problem","props","rendered","mode","template","recipient","siteBase","hostId","messageText","facts","htmlBytes","TextEncoder","encode","html","blocks","size","plainText","readScreenRows","hostRef","snapshot","collection","select","docs","doc","kind","get","deletedAt","roleRefusal","host","uid","role","status","error","roomRefusal","rows","used","strings","entry","recordOf","screen","versionId","copyFields","emailSubject","emailPreheader","emailSubjectVariants","emailPreheaderVariants","createEmailDesignDraftWriter","deps","firestore","app","mintId","refusal","exists","query","check","write","request","packed","db","screenRef","runTransaction","tx","existing","Promise","all","replayed","row","version","screenId","displayName","stamps","createdAt","now","updatedAt","createdBy","copy","create","nameLower","Buffer","from","emailDesignDraftWriter","registerEmailDesignDraftWriter","pluginId"],"mappings":"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"}
|