@odla-ai/chapter 0.60.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/{chunk-J7ES2OWN.js → chunk-24L7FANC.js} +3 -2
- package/dist/{chunk-J7ES2OWN.js.map → chunk-24L7FANC.js.map} +1 -1
- package/dist/{chunk-QNP3UEIL.js → chunk-PKVHXKGJ.js} +3 -1
- package/dist/chunk-PKVHXKGJ.js.map +1 -0
- package/dist/{chunk-D5UR5H6R.js → chunk-RTJKUOFE.js} +2 -2
- package/dist/{copy-context-BP3G1iBd.d.ts → copy-context-nsw2DwSK.d.ts} +1 -1
- package/dist/index.cjs +112 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -8
- package/dist/index.d.ts +51 -8
- package/dist/index.js +88 -41
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/admin/index.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +160 -115
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +1 -1
- package/dist/worker/index.d.ts +1 -1
- package/dist/worker/index.js +89 -44
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-QNP3UEIL.js.map +0 -1
- /package/dist/{chunk-D5UR5H6R.js.map → chunk-RTJKUOFE.js.map} +0 -0
package/dist/worker/index.d.cts
CHANGED
|
@@ -122,7 +122,7 @@ interface ChapterCopy {
|
|
|
122
122
|
availability: TextFields<"loading" | "loadFailed" | "saved" | "title" | "days" | "startHour" | "endHour" | "slotMinutes" | "minNoticeHours" | "windowDays" | "timezone" | "summaryTemplate" | "save"> & {
|
|
123
123
|
dayLabels: readonly string[];
|
|
124
124
|
};
|
|
125
|
-
email: TextFields<"loading" | "loadFailed" | "saved" | "sent" | "testFailed" | "delivery" | "notificationAddress" | "replyTo" | "fromName" | "fromNameHint" | "spamRisk" | "debugInbox" | "debugInboxHint" | "sendTest" | "enabled" | "subject" | "body" | "save" | "sendLog" | "sentColumn" | "templateColumn" | "statusColumn" | "toColumn" | "failed" | "redirected" | "delivered">;
|
|
125
|
+
email: TextFields<"loading" | "loadFailed" | "saved" | "sent" | "testFailed" | "delivery" | "notificationAddress" | "replyTo" | "fromName" | "fromNameHint" | "spamRisk" | "debugInbox" | "debugInboxHint" | "sendTest" | "enabled" | "subject" | "body" | "htmlBody" | "htmlBodyHint" | "save" | "sendLog" | "sentColumn" | "templateColumn" | "statusColumn" | "toColumn" | "failed" | "redirected" | "delivered">;
|
|
126
126
|
meetings: TextFields<"loading" | "loadFailed" | "cancelConfirm" | "newStartPrompt" | "parseFailed" | "agenda" | "empty" | "unknown" | "drift" | "meet" | "reschedule" | "cancel">;
|
|
127
127
|
admission: TextFields<"title" | "description" | "email" | "decision" | "expires" | "note" | "waive" | "require" | "add" | "ledger" | "loading" | "consumed" | "revoked" | "expired" | "active" | "local" | "parent" | "revoke">;
|
|
128
128
|
network: AdminNetworkCopy;
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -122,7 +122,7 @@ interface ChapterCopy {
|
|
|
122
122
|
availability: TextFields<"loading" | "loadFailed" | "saved" | "title" | "days" | "startHour" | "endHour" | "slotMinutes" | "minNoticeHours" | "windowDays" | "timezone" | "summaryTemplate" | "save"> & {
|
|
123
123
|
dayLabels: readonly string[];
|
|
124
124
|
};
|
|
125
|
-
email: TextFields<"loading" | "loadFailed" | "saved" | "sent" | "testFailed" | "delivery" | "notificationAddress" | "replyTo" | "fromName" | "fromNameHint" | "spamRisk" | "debugInbox" | "debugInboxHint" | "sendTest" | "enabled" | "subject" | "body" | "save" | "sendLog" | "sentColumn" | "templateColumn" | "statusColumn" | "toColumn" | "failed" | "redirected" | "delivered">;
|
|
125
|
+
email: TextFields<"loading" | "loadFailed" | "saved" | "sent" | "testFailed" | "delivery" | "notificationAddress" | "replyTo" | "fromName" | "fromNameHint" | "spamRisk" | "debugInbox" | "debugInboxHint" | "sendTest" | "enabled" | "subject" | "body" | "htmlBody" | "htmlBodyHint" | "save" | "sendLog" | "sentColumn" | "templateColumn" | "statusColumn" | "toColumn" | "failed" | "redirected" | "delivered">;
|
|
126
126
|
meetings: TextFields<"loading" | "loadFailed" | "cancelConfirm" | "newStartPrompt" | "parseFailed" | "agenda" | "empty" | "unknown" | "drift" | "meet" | "reschedule" | "cancel">;
|
|
127
127
|
admission: TextFields<"title" | "description" | "email" | "decision" | "expires" | "note" | "waive" | "require" | "add" | "ledger" | "loading" | "consumed" | "revoked" | "expired" | "active" | "local" | "parent" | "revoke">;
|
|
128
128
|
network: AdminNetworkCopy;
|
package/dist/worker/index.js
CHANGED
|
@@ -1642,18 +1642,72 @@ function isRetiredEmailCopy(subject, text10) {
|
|
|
1642
1642
|
// src/email.ts
|
|
1643
1643
|
import { validateEmailCopy } from "@odla-ai/crm";
|
|
1644
1644
|
|
|
1645
|
+
// src/email-render.ts
|
|
1646
|
+
import { renderTemplate, templateLiveVars, templateSectionIssues, templateSectionNames, templateVars } from "@odla-ai/crm";
|
|
1647
|
+
|
|
1648
|
+
// src/email-html.ts
|
|
1649
|
+
var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
|
|
1650
|
+
function escapeHtml(s) {
|
|
1651
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1652
|
+
}
|
|
1653
|
+
function lineHtml(line) {
|
|
1654
|
+
let out = "", last = 0;
|
|
1655
|
+
for (const match of line.matchAll(URL_RE)) {
|
|
1656
|
+
out += escapeHtml(line.slice(last, match.index));
|
|
1657
|
+
const url = escapeHtml(match[0]);
|
|
1658
|
+
out += `<a href="${url}">${url}</a>`;
|
|
1659
|
+
last = match.index + match[0].length;
|
|
1660
|
+
}
|
|
1661
|
+
return out + escapeHtml(line.slice(last));
|
|
1662
|
+
}
|
|
1663
|
+
function textToHtml(text10) {
|
|
1664
|
+
const paragraphs = text10.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
1665
|
+
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
1666
|
+
return `<!doctype html>
|
|
1667
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
1668
|
+
<body style="margin:0;padding:24px 16px;background:#ffffff;color:#1a1a1a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5">
|
|
1669
|
+
${body}
|
|
1670
|
+
</body></html>`;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/email-render.ts
|
|
1674
|
+
function render(template, vars) {
|
|
1675
|
+
return renderTemplate(template, vars);
|
|
1676
|
+
}
|
|
1677
|
+
var HTML_BODY_LIMIT = 2e5;
|
|
1678
|
+
function renderHtmlBody(template, vars) {
|
|
1679
|
+
const escaped = Object.fromEntries(Object.entries(vars).map(([name, value]) => [name, escapeHtml(value).replace(/\r?\n/g, "<br>")]));
|
|
1680
|
+
return renderTemplate(template, escaped);
|
|
1681
|
+
}
|
|
1682
|
+
function htmlBodyIssues(html, options) {
|
|
1683
|
+
const issues = [];
|
|
1684
|
+
if (html.length > HTML_BODY_LIMIT) issues.push({ code: "html-invalid", message: `Keep the HTML body within ${HTML_BODY_LIMIT.toLocaleString("en-US")} characters.` });
|
|
1685
|
+
for (const message of templateSectionIssues(html)) issues.push({ code: "section-invalid", message: `${message} (HTML body)` });
|
|
1686
|
+
const unknown = new Set(templateVars(html).filter((variable) => options.allowedVariables && !options.allowedVariables.includes(variable)));
|
|
1687
|
+
for (const variable of unknown) issues.push({ code: "variable-unknown", variable, message: `This event does not supply {{${variable}}} (HTML body).` });
|
|
1688
|
+
if (options.values) {
|
|
1689
|
+
for (const name of templateSectionNames(html)) if (typeof options.values[name] !== "string") issues.push({ code: "value-missing", variable: name, message: `Missing event value: ${name} (HTML body).` });
|
|
1690
|
+
for (const variable of templateLiveVars(html, options.values)) {
|
|
1691
|
+
if (unknown.has(variable)) continue;
|
|
1692
|
+
const value = options.values[variable];
|
|
1693
|
+
if (typeof value !== "string" || !value.trim() && !options.optionalVariables?.includes(variable)) issues.push({ code: "value-missing", variable, message: `Missing event value: ${variable} (HTML body).` });
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
return issues;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1645
1699
|
// src/email-events.ts
|
|
1646
1700
|
var identity = ["firstName", "lastName", "email", "phone", "state"];
|
|
1647
1701
|
var policy = ["refundPolicyText", "commitmentText", "normsText"];
|
|
1648
|
-
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
|
|
1649
|
-
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
|
|
1702
|
+
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
|
|
1703
|
+
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
|
|
1650
1704
|
var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
|
|
1651
1705
|
var CHAPTER_EMAIL_EVENTS = {
|
|
1652
1706
|
adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
|
|
1653
1707
|
submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
|
|
1654
1708
|
paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
|
|
1655
1709
|
prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
|
|
1656
|
-
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
|
|
1710
|
+
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName", "namedSeat", "namedSeatRecipientName"]),
|
|
1657
1711
|
namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
|
|
1658
1712
|
seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
|
|
1659
1713
|
};
|
|
@@ -1668,9 +1722,6 @@ function chapterEmailEvents(chapter) {
|
|
|
1668
1722
|
}
|
|
1669
1723
|
|
|
1670
1724
|
// src/email.ts
|
|
1671
|
-
function render(template, vars) {
|
|
1672
|
-
return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
|
|
1673
|
-
}
|
|
1674
1725
|
function groupVars(group, vars) {
|
|
1675
1726
|
return {
|
|
1676
1727
|
...vars,
|
|
@@ -1703,7 +1754,9 @@ function planDelivery(input) {
|
|
|
1703
1754
|
values: vars,
|
|
1704
1755
|
tierId: vars.tierId
|
|
1705
1756
|
});
|
|
1706
|
-
|
|
1757
|
+
const html = typeof tpl.html === "string" && tpl.html.trim() ? tpl.html : void 0;
|
|
1758
|
+
const htmlIssues = html ? htmlBodyIssues(html, { allowedVariables: event2?.variables, optionalVariables: event2?.optionalVariables, values: vars }) : [];
|
|
1759
|
+
if (issues.length || htmlIssues.length) return { deliver: false, reason: "template-invalid", issues: [...issues, ...htmlIssues].map((issue) => issue.message) };
|
|
1707
1760
|
const isProd = input.envName === "prod";
|
|
1708
1761
|
const redirect = !isProd && !!input.group.debugEmail;
|
|
1709
1762
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
@@ -1716,32 +1769,9 @@ function planDelivery(input) {
|
|
|
1716
1769
|
const text10 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1717
1770
|
|
|
1718
1771
|
` + renderedText : renderedText;
|
|
1719
|
-
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
// src/email-html.ts
|
|
1723
|
-
var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
|
|
1724
|
-
function escapeHtml(s) {
|
|
1725
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1726
|
-
}
|
|
1727
|
-
function lineHtml(line) {
|
|
1728
|
-
let out = "", last = 0;
|
|
1729
|
-
for (const match of line.matchAll(URL_RE)) {
|
|
1730
|
-
out += escapeHtml(line.slice(last, match.index));
|
|
1731
|
-
const url = escapeHtml(match[0]);
|
|
1732
|
-
out += `<a href="${url}">${url}</a>`;
|
|
1733
|
-
last = match.index + match[0].length;
|
|
1734
|
-
}
|
|
1735
|
-
return out + escapeHtml(line.slice(last));
|
|
1736
|
-
}
|
|
1737
|
-
function textToHtml(text10) {
|
|
1738
|
-
const paragraphs = text10.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
|
|
1739
|
-
const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
|
|
1740
|
-
return `<!doctype html>
|
|
1741
|
-
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
1742
|
-
<body style="margin:0;padding:24px 16px;background:#ffffff;color:#1a1a1a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5">
|
|
1743
|
-
${body}
|
|
1744
|
-
</body></html>`;
|
|
1772
|
+
const renderedHtml = html ? renderHtmlBody(html, vars) : void 0;
|
|
1773
|
+
const htmlOut = renderedHtml && redirect ? `<p style="font:13px monospace;color:#555;margin:0 0 12px">(dev redirect; original recipient: ${escapeHtml(input.to)})</p>` + renderedHtml : renderedHtml;
|
|
1774
|
+
return { deliver: true, transport, to, subject, text: text10, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
|
|
1745
1775
|
}
|
|
1746
1776
|
|
|
1747
1777
|
// src/email-deliverability.ts
|
|
@@ -1873,10 +1903,14 @@ async function frozenCheckoutVariables(db, app) {
|
|
|
1873
1903
|
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
1874
1904
|
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
1875
1905
|
}
|
|
1906
|
+
const seat = quote?.namedSeat, seatCents = quote?.lineItems?.namedSeatCents;
|
|
1907
|
+
const namedSeatRecipientName = typeof seat?.recipientName === "string" ? seat.recipientName.trim() : "";
|
|
1876
1908
|
return {
|
|
1877
1909
|
paymentAmount: formatted,
|
|
1878
1910
|
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
1879
|
-
tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
|
|
1911
|
+
tierName: typeof quote?.tierName === "string" ? quote.tierName : "",
|
|
1912
|
+
namedSeat: namedSeatRecipientName ? Number.isSafeInteger(seatCents) && Number(seatCents) > 0 ? "paid" : "included" : "",
|
|
1913
|
+
namedSeatRecipientName
|
|
1880
1914
|
};
|
|
1881
1915
|
}
|
|
1882
1916
|
async function catalogTierName(db, group, app) {
|
|
@@ -1934,6 +1968,8 @@ function sampleVariables(origin, timeZone, now = Date.now()) {
|
|
|
1934
1968
|
purchaserName: "Sample Purchaser",
|
|
1935
1969
|
recipientName: "Sample Recipient",
|
|
1936
1970
|
chapterName: "Sample Chapter",
|
|
1971
|
+
namedSeat: "paid",
|
|
1972
|
+
namedSeatRecipientName: "Sample Recipient",
|
|
1937
1973
|
acceptBy: new Date(now + 7 * 864e5).toISOString().slice(0, 10),
|
|
1938
1974
|
joinUrl: `${origin}/join?seat=sample`
|
|
1939
1975
|
};
|
|
@@ -2032,7 +2068,7 @@ async function sendTemplated(deps, input) {
|
|
|
2032
2068
|
let messageId;
|
|
2033
2069
|
try {
|
|
2034
2070
|
const text10 = String(row2.body);
|
|
2035
|
-
const html = deps.renderHtml === false ? void 0 : (deps.renderHtml ?? textToHtml)(text10);
|
|
2071
|
+
const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text10);
|
|
2036
2072
|
const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
|
|
2037
2073
|
const response = await deps.sender.send({
|
|
2038
2074
|
from: fromAddress(deps.from, input.group.fromName),
|
|
@@ -12508,7 +12544,7 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
12508
12544
|
import { getConfig, saveConfig, validateEmailCopy as validateEmailCopy4 } from "@odla-ai/crm";
|
|
12509
12545
|
|
|
12510
12546
|
// src/email-readiness.ts
|
|
12511
|
-
import { isEmailTierName as isEmailTierName2, validateEmailCopy as validateEmailCopy3 } from "@odla-ai/crm";
|
|
12547
|
+
import { isEmailTierName as isEmailTierName2, templateSectionNames as templateSectionNames2, templateVars as templateVars2, validateEmailCopy as validateEmailCopy3 } from "@odla-ai/crm";
|
|
12512
12548
|
|
|
12513
12549
|
// src/email-spam-signals.ts
|
|
12514
12550
|
var TRIGGER_PHRASES = [
|
|
@@ -12545,6 +12581,7 @@ var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|
|
|
|
12545
12581
|
function emailSpamSignals(copy) {
|
|
12546
12582
|
const subject = typeof copy.subject === "string" ? copy.subject : "";
|
|
12547
12583
|
const text10 = typeof copy.text === "string" ? copy.text : "";
|
|
12584
|
+
const html = typeof copy.html === "string" ? copy.html : "";
|
|
12548
12585
|
const out = [];
|
|
12549
12586
|
const letters = strip(subject).replace(/[^A-Za-z]/g, "");
|
|
12550
12587
|
const shouting = letters.length >= 6 && letters.replace(/[^A-Z]/g, "").length / letters.length > 0.5 || words(subject).some((word) => /^[A-Z]{4,}$/.test(word.replace(/[^A-Za-z]/g, "")));
|
|
@@ -12557,7 +12594,8 @@ function emailSpamSignals(copy) {
|
|
|
12557
12594
|
if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
|
|
12558
12595
|
if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(text10))) out.push({ code: "body-caps", message: "The body shouts a run of capitalised words; use sentence case." });
|
|
12559
12596
|
if (/!{2,}/.test(text10)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
|
|
12560
|
-
const urls = [
|
|
12597
|
+
const urls = [...`${text10}
|
|
12598
|
+
${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/".*$/, ""));
|
|
12561
12599
|
const hosts = urls.map((url) => {
|
|
12562
12600
|
try {
|
|
12563
12601
|
return new URL(url).hostname;
|
|
@@ -12602,10 +12640,14 @@ function validateEmailReadiness(input) {
|
|
|
12602
12640
|
const template = input.templates[key3];
|
|
12603
12641
|
const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : validateEmailCopy3(template, { allowedVariables: event2.variables });
|
|
12604
12642
|
if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
|
|
12643
|
+
const html = typeof template?.html === "string" && template.html.trim() ? template.html : "";
|
|
12644
|
+
if (html) problems.push(...htmlBodyIssues(html, { allowedVariables: event2.variables }));
|
|
12605
12645
|
if (template) {
|
|
12606
|
-
const
|
|
12607
|
-
${template.text ?? ""}
|
|
12608
|
-
|
|
12646
|
+
const copy = `${template.subject ?? ""}
|
|
12647
|
+
${template.text ?? ""}
|
|
12648
|
+
${html}`;
|
|
12649
|
+
const sectionOnly = new Set(templateSectionNames2(copy));
|
|
12650
|
+
for (const name of templateVars2(copy)) if (name in input.groupValues && !input.groupValues[name]?.trim() && !sectionOnly.has(name)) {
|
|
12609
12651
|
problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
|
|
12610
12652
|
}
|
|
12611
12653
|
}
|
|
@@ -12634,6 +12676,7 @@ async function emailSettings(db, ctx, env) {
|
|
|
12634
12676
|
const emailTemplates = Object.fromEntries(Object.keys(events).filter((key3) => key3 in stored).map((key3) => [key3, {
|
|
12635
12677
|
subject: str2(stored[key3]?.subject),
|
|
12636
12678
|
text: str2(stored[key3]?.text),
|
|
12679
|
+
html: str2(stored[key3]?.html),
|
|
12637
12680
|
enabled: stored[key3]?.enabled
|
|
12638
12681
|
}]));
|
|
12639
12682
|
let tiers = [], tiersUnavailable = false;
|
|
@@ -12680,12 +12723,14 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
12680
12723
|
if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
|
|
12681
12724
|
const t = value;
|
|
12682
12725
|
if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
|
|
12683
|
-
const subject = str2(t.subject).trim(), text10 = str2(t.text);
|
|
12726
|
+
const subject = str2(t.subject).trim(), text10 = str2(t.text), html = str2(t.html);
|
|
12727
|
+
if (t.html !== void 0 && typeof t.html !== "string") return json({ error: `Invalid HTML body: ${key3}` }, 400);
|
|
12684
12728
|
const issues = validateEmailCopy4({ subject, text: text10 }, { allowedVariables: events[key3].variables });
|
|
12685
|
-
|
|
12686
|
-
|
|
12729
|
+
const htmlIssues = html.trim() ? htmlBodyIssues(html, { allowedVariables: events[key3].variables }) : [];
|
|
12730
|
+
if (t.enabled && (issues.length || htmlIssues.length) || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code)) || html.length > HTML_BODY_LIMIT) {
|
|
12731
|
+
return json({ error: `${events[key3].label} is incomplete.`, errors: [...issues, ...htmlIssues].map((issue) => issue.message) }, 400);
|
|
12687
12732
|
}
|
|
12688
|
-
clean[key3] = { subject, text: text10, enabled: t.enabled };
|
|
12733
|
+
clean[key3] = { subject, text: text10, ...html.trim() ? { html } : {}, enabled: t.enabled };
|
|
12689
12734
|
}
|
|
12690
12735
|
const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
|
|
12691
12736
|
const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
|