// Shared email-sender module for the generated Next.js project. // // Why this exists: order-notification and low-stock-alert API routes // both render `{{token}}` templates and dispatch to one of several // providers (Postmark / SendGrid / Resend / SMTP fallback). Without a // shared helper each endpoint duplicates the provider switch + retry // + logging, which led to drift the last time we touched it (postmark // wasn't supported in order-notification, sendgrid wasn't supported // in low-stock, etc.). One helper means one place to fix. // // Emits `utils/ecommerce/email-sender.js` as plain CommonJS so the // API routes (which are `pages/api/**` and use mixed import styles) // can `require()` it without a TypeScript transpile step. The // generated file is otherwise self-contained — no React, no Next // runtime — and safe to import from any server-side context // (workflow server segments, webhooks, the data-api itself). import { UIDLEcommerceSettings } from '@teleporthq/teleport-types' import { EmailSender } from '@teleporthq/teleport-shared' const POSTMARK_PROVIDER = 'postmark' const SENDGRID_PROVIDER = 'sendgrid' const RESEND_PROVIDER = 'resend' // Render `{{token}}` placeholders against a flat payload of values. // The regex matches `{{ word }}` (whitespace-tolerant, word chars only) // so surrounding HTML markup that contains lone `{` / `}` is left // untouched. Missing tokens render as empty string — never as the // literal `{{token}}`, which would leak the template into the email. const RENDER_TEMPLATE_FN = `function renderTemplate(template, payload) { if (!template) return '' return String(template).replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, function(_match, key) { var v = payload && payload[key] if (v == null) return '' return String(v) }) }` // Expand the repeating-row blocks a BUILDER email template serializes an // array-mapper into: `…{{field}}…`. The inner // body is repeated once per row of `lists[KEY]`, with the row's `{{field}}` // tokens replaced by the (HTML-escaped) item value. Tokens the row doesn't // carry are left verbatim so the page-level `renderTemplate` below can still // resolve them (e.g. `{{companyName}}` inside a row). // // MUST run BEFORE `renderTemplate`: that one blanks every unknown token, so a // row template reaching it un-expanded renders exactly once with every // per-item value empty — which is precisely the "Items ordered section is // blank" defect this exists to prevent. // // PAIRED EDIT: identical logic lives in the workflow runtime // (`executor-generator.ts` `expandListBlocks`), in teleport-gui // (`order-side-effects-helper.ts` `EXPAND_LIST_BLOCKS_JS`) and in // teleport-services-worker (`adapters/email/_bridge.ts`). Keep in sync. const EXPAND_LIST_BLOCKS_FN = `function expandListBlocks(template, lists) { if (typeof template !== 'string' || template.indexOf('([\\s\\S]*?)/g, function(_m, key, body) { var rows = lists && lists[key] if (!Array.isArray(rows)) return '' var out = '' for (var r = 0; r < rows.length; r++) { var row = rows[r] || {} out += body.replace(/\\{\\{\\s*([\\w.-]+)\\s*\\}\\}/g, function(mm, field) { return (Object.prototype.hasOwnProperty.call(row, field) && row[field] != null) ? escRow(row[field]) : mm }) } return out }) }` // True when a merchant template renders its own item list — either as a // builder array-mapper block or via the legacy `{{itemsList}}` blob. The // endpoints use this to skip appending a second, auto-generated list. const HAS_OWN_ITEM_LIST_FN = `function hasOwnItemList(template) { if (!template) return false var t = String(template) return t.indexOf('