@antelopejs/dms-mailing 0.3.4 → 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.
Files changed (707) hide show
  1. package/README.md +232 -22
  2. package/dist/automation/email-event-trigger.js +2 -2
  3. package/dist/automation/email-event-trigger.js.map +1 -1
  4. package/dist/automation/send-template-action.js +2 -2
  5. package/dist/automation/send-template-action.js.map +1 -1
  6. package/dist/config.js +3 -1
  7. package/dist/config.js.map +1 -1
  8. package/dist/constants.d.ts +10 -1
  9. package/dist/constants.js +15 -3
  10. package/dist/constants.js.map +1 -1
  11. package/dist/crons/attention.d.ts +3 -0
  12. package/dist/crons/attention.js +19 -0
  13. package/dist/crons/attention.js.map +1 -0
  14. package/dist/crons/index.d.ts +2 -0
  15. package/dist/crons/index.js +2 -0
  16. package/dist/crons/index.js.map +1 -1
  17. package/dist/crons/jobs.d.ts +12 -0
  18. package/dist/crons/jobs.js +27 -0
  19. package/dist/crons/jobs.js.map +1 -0
  20. package/dist/crons/retention.d.ts +2 -4
  21. package/dist/crons/retention.js +7 -22
  22. package/dist/crons/retention.js.map +1 -1
  23. package/dist/data/displays.d.ts +5 -0
  24. package/dist/data/displays.js +23 -0
  25. package/dist/data/displays.js.map +1 -0
  26. package/dist/data/index.d.ts +1 -0
  27. package/dist/data/index.js +1 -0
  28. package/dist/data/index.js.map +1 -1
  29. package/dist/data/locale-fallback.d.ts +3 -0
  30. package/dist/data/locale-fallback.js +20 -0
  31. package/dist/data/locale-fallback.js.map +1 -0
  32. package/dist/data/read-only-routes.d.ts +7 -0
  33. package/dist/data/read-only-routes.js +13 -0
  34. package/dist/data/read-only-routes.js.map +1 -0
  35. package/dist/data/sends-table.d.ts +19 -5
  36. package/dist/data/sends-table.js +185 -17
  37. package/dist/data/sends-table.js.map +1 -1
  38. package/dist/data/suppressions-table.d.ts +24 -0
  39. package/dist/data/suppressions-table.js +120 -0
  40. package/dist/data/suppressions-table.js.map +1 -0
  41. package/dist/data/templates-table.d.ts +20 -2
  42. package/dist/data/templates-table.js +188 -31
  43. package/dist/data/templates-table.js.map +1 -1
  44. package/dist/data-types/categories-type.d.ts +11 -13
  45. package/dist/data-types/categories-type.js +3 -3
  46. package/dist/data-types/categories-type.js.map +1 -1
  47. package/dist/data-types/category-type.js +1 -1
  48. package/dist/data-types/category-type.js.map +1 -1
  49. package/dist/db/models/index.d.ts +3 -0
  50. package/dist/db/models/index.js +3 -0
  51. package/dist/db/models/index.js.map +1 -1
  52. package/dist/db/models/sends.model.d.ts +12 -1
  53. package/dist/db/models/sends.model.js +64 -0
  54. package/dist/db/models/sends.model.js.map +1 -1
  55. package/dist/db/models/settings.model.d.ts +0 -1
  56. package/dist/db/models/settings.model.js +0 -7
  57. package/dist/db/models/settings.model.js.map +1 -1
  58. package/dist/db/models/suppressions.model.d.ts +28 -0
  59. package/dist/db/models/suppressions.model.js +46 -0
  60. package/dist/db/models/suppressions.model.js.map +1 -0
  61. package/dist/db/models/template-versions.model.d.ts +24 -0
  62. package/dist/db/models/template-versions.model.js +17 -0
  63. package/dist/db/models/template-versions.model.js.map +1 -0
  64. package/dist/db/models/templates.model.d.ts +2 -0
  65. package/dist/db/models/templates.model.js +3 -0
  66. package/dist/db/models/templates.model.js.map +1 -1
  67. package/dist/db/models/webhook-status.model.d.ts +24 -0
  68. package/dist/db/models/webhook-status.model.js +21 -0
  69. package/dist/db/models/webhook-status.model.js.map +1 -0
  70. package/dist/db/tables/index.d.ts +3 -0
  71. package/dist/db/tables/index.js +3 -0
  72. package/dist/db/tables/index.js.map +1 -1
  73. package/dist/db/tables/send-events.table.d.ts +2 -2
  74. package/dist/db/tables/send-events.table.js.map +1 -1
  75. package/dist/db/tables/sends.table.d.ts +10 -2
  76. package/dist/db/tables/sends.table.js +34 -1
  77. package/dist/db/tables/sends.table.js.map +1 -1
  78. package/dist/db/tables/suppressions.table.d.ts +12 -0
  79. package/dist/db/tables/suppressions.table.js +53 -0
  80. package/dist/db/tables/suppressions.table.js.map +1 -0
  81. package/dist/db/tables/template-versions.table.d.ts +13 -0
  82. package/dist/db/tables/template-versions.table.js +59 -0
  83. package/dist/db/tables/template-versions.table.js.map +1 -0
  84. package/dist/db/tables/templates.table.d.ts +11 -0
  85. package/dist/db/tables/templates.table.js +45 -0
  86. package/dist/db/tables/templates.table.js.map +1 -1
  87. package/dist/db/tables/webhook-status.table.d.ts +14 -0
  88. package/dist/db/tables/webhook-status.table.js +58 -0
  89. package/dist/db/tables/webhook-status.table.js.map +1 -0
  90. package/dist/engine/changes.d.ts +12 -0
  91. package/dist/engine/changes.js +107 -0
  92. package/dist/engine/changes.js.map +1 -0
  93. package/dist/engine/conditions.js +0 -2
  94. package/dist/engine/conditions.js.map +1 -1
  95. package/dist/engine/coverage.d.ts +1 -0
  96. package/dist/engine/coverage.js +6 -2
  97. package/dist/engine/coverage.js.map +1 -1
  98. package/dist/engine/fallback-html.d.ts +1 -1
  99. package/dist/engine/fallback-html.js +51 -34
  100. package/dist/engine/fallback-html.js.map +1 -1
  101. package/dist/engine/index.d.ts +3 -0
  102. package/dist/engine/index.js +3 -0
  103. package/dist/engine/index.js.map +1 -1
  104. package/dist/engine/plain-text.d.ts +3 -0
  105. package/dist/engine/plain-text.js +60 -0
  106. package/dist/engine/plain-text.js.map +1 -0
  107. package/dist/engine/resolve.d.ts +2 -1
  108. package/dist/engine/resolve.js +17 -9
  109. package/dist/engine/resolve.js.map +1 -1
  110. package/dist/engine/system-variables.d.ts +10 -0
  111. package/dist/engine/system-variables.js +70 -0
  112. package/dist/engine/system-variables.js.map +1 -0
  113. package/dist/engine/tokens.d.ts +2 -1
  114. package/dist/engine/tokens.js +6 -2
  115. package/dist/engine/tokens.js.map +1 -1
  116. package/dist/hooks/tenant-export.js +13 -1
  117. package/dist/hooks/tenant-export.js.map +1 -1
  118. package/dist/implementations/dms-mailing/index.d.ts +1 -1
  119. package/dist/implementations/dms-mailing/index.js +2 -2
  120. package/dist/implementations/dms-mailing/index.js.map +1 -1
  121. package/dist/index.js +13 -11
  122. package/dist/index.js.map +1 -1
  123. package/dist/pages/editor.js +9 -2
  124. package/dist/pages/editor.js.map +1 -1
  125. package/dist/pages/index.d.ts +1 -0
  126. package/dist/pages/index.js +1 -0
  127. package/dist/pages/index.js.map +1 -1
  128. package/dist/pages/module.d.ts +2 -0
  129. package/dist/pages/module.js +26 -2
  130. package/dist/pages/module.js.map +1 -1
  131. package/dist/pages/overview.d.ts +2 -6
  132. package/dist/pages/overview.js +109 -52
  133. package/dist/pages/overview.js.map +1 -1
  134. package/dist/pages/sends.d.ts +3 -4
  135. package/dist/pages/sends.js +197 -60
  136. package/dist/pages/sends.js.map +1 -1
  137. package/dist/pages/settings/form.js +114 -53
  138. package/dist/pages/settings/form.js.map +1 -1
  139. package/dist/pages/settings/page.d.ts +0 -1
  140. package/dist/pages/settings/page.js +8 -11
  141. package/dist/pages/settings/page.js.map +1 -1
  142. package/dist/pages/suppressions.d.ts +9 -0
  143. package/dist/pages/suppressions.js +151 -0
  144. package/dist/pages/suppressions.js.map +1 -0
  145. package/dist/pages/templates.d.ts +0 -1
  146. package/dist/pages/templates.js +115 -41
  147. package/dist/pages/templates.js.map +1 -1
  148. package/dist/permissions.js +2 -2
  149. package/dist/permissions.js.map +1 -1
  150. package/dist/quick-actions.js +7 -4
  151. package/dist/quick-actions.js.map +1 -1
  152. package/dist/routes/events.d.ts +8 -5
  153. package/dist/routes/events.js +55 -24
  154. package/dist/routes/events.js.map +1 -1
  155. package/dist/routes/index.d.ts +2 -0
  156. package/dist/routes/index.js +2 -0
  157. package/dist/routes/index.js.map +1 -1
  158. package/dist/routes/metrics.d.ts +60 -23
  159. package/dist/routes/metrics.js +231 -83
  160. package/dist/routes/metrics.js.map +1 -1
  161. package/dist/routes/period.d.ts +2 -1
  162. package/dist/routes/period.js +9 -3
  163. package/dist/routes/period.js.map +1 -1
  164. package/dist/routes/sends.d.ts +49 -8
  165. package/dist/routes/sends.js +239 -24
  166. package/dist/routes/sends.js.map +1 -1
  167. package/dist/routes/settings.d.ts +44 -5
  168. package/dist/routes/settings.js +190 -15
  169. package/dist/routes/settings.js.map +1 -1
  170. package/dist/routes/suppressions.d.ts +16 -0
  171. package/dist/routes/suppressions.js +92 -0
  172. package/dist/routes/suppressions.js.map +1 -0
  173. package/dist/routes/template-versions.d.ts +28 -0
  174. package/dist/routes/template-versions.js +87 -0
  175. package/dist/routes/template-versions.js.map +1 -0
  176. package/dist/routes/templates.d.ts +60 -6
  177. package/dist/routes/templates.js +204 -35
  178. package/dist/routes/templates.js.map +1 -1
  179. package/dist/services/attention.d.ts +19 -6
  180. package/dist/services/attention.js +177 -12
  181. package/dist/services/attention.js.map +1 -1
  182. package/dist/services/catalog.d.ts +3 -0
  183. package/dist/services/catalog.js +25 -0
  184. package/dist/services/catalog.js.map +1 -0
  185. package/dist/services/categories.d.ts +8 -32
  186. package/dist/services/categories.js +14 -25
  187. package/dist/services/categories.js.map +1 -1
  188. package/dist/services/checked-send.d.ts +9 -0
  189. package/dist/services/checked-send.js +54 -0
  190. package/dist/services/checked-send.js.map +1 -0
  191. package/dist/services/events.d.ts +5 -0
  192. package/dist/services/events.js +102 -9
  193. package/dist/services/events.js.map +1 -1
  194. package/dist/services/metrics.d.ts +17 -6
  195. package/dist/services/metrics.js +57 -18
  196. package/dist/services/metrics.js.map +1 -1
  197. package/dist/services/notify.d.ts +18 -10
  198. package/dist/services/notify.js +82 -37
  199. package/dist/services/notify.js.map +1 -1
  200. package/dist/services/provider-banner.d.ts +14 -0
  201. package/dist/services/provider-banner.js +115 -0
  202. package/dist/services/provider-banner.js.map +1 -0
  203. package/dist/services/render.js.map +1 -1
  204. package/dist/services/send-stat-items.d.ts +7 -0
  205. package/dist/services/send-stat-items.js +127 -0
  206. package/dist/services/send-stat-items.js.map +1 -0
  207. package/dist/services/send-stats.d.ts +23 -0
  208. package/dist/services/send-stats.js +63 -0
  209. package/dist/services/send-stats.js.map +1 -0
  210. package/dist/services/send.d.ts +18 -3
  211. package/dist/services/send.js +81 -17
  212. package/dist/services/send.js.map +1 -1
  213. package/dist/services/settings.d.ts +5 -0
  214. package/dist/services/settings.js +28 -7
  215. package/dist/services/settings.js.map +1 -1
  216. package/dist/services/starters.d.ts +15 -0
  217. package/dist/services/starters.js +264 -0
  218. package/dist/services/starters.js.map +1 -0
  219. package/dist/services/suppressions.d.ts +20 -0
  220. package/dist/services/suppressions.js +53 -0
  221. package/dist/services/suppressions.js.map +1 -0
  222. package/dist/services/template-attention.d.ts +4 -0
  223. package/dist/services/template-attention.js +43 -0
  224. package/dist/services/template-attention.js.map +1 -0
  225. package/dist/services/template-stats.d.ts +27 -0
  226. package/dist/services/template-stats.js +59 -0
  227. package/dist/services/template-stats.js.map +1 -0
  228. package/dist/services/templates.d.ts +31 -4
  229. package/dist/services/templates.js +240 -21
  230. package/dist/services/templates.js.map +1 -1
  231. package/dist/services/webhook-status.d.ts +27 -0
  232. package/dist/services/webhook-status.js +134 -0
  233. package/dist/services/webhook-status.js.map +1 -0
  234. package/dist/types/blocks.d.ts +1 -0
  235. package/dist/types/index.d.ts +2 -0
  236. package/dist/types/index.js +2 -0
  237. package/dist/types/index.js.map +1 -1
  238. package/dist/types/sends.d.ts +10 -1
  239. package/dist/types/sends.js +35 -2
  240. package/dist/types/sends.js.map +1 -1
  241. package/dist/types/suppressions.d.ts +9 -0
  242. package/dist/types/suppressions.js +10 -0
  243. package/dist/types/suppressions.js.map +1 -0
  244. package/dist/types/webhook.d.ts +17 -0
  245. package/dist/types/webhook.js +3 -0
  246. package/dist/types/webhook.js.map +1 -0
  247. package/dist/validation/settings.schema.d.ts +144 -9
  248. package/dist/validation/settings.schema.js +113 -16
  249. package/dist/validation/settings.schema.js.map +1 -1
  250. package/dist/validation/templates.schema.d.ts +137 -10
  251. package/dist/validation/templates.schema.js +28 -2
  252. package/dist/validation/templates.schema.js.map +1 -1
  253. package/frontend-vue/app/build/components/FormFieldRow.vue +90 -0
  254. package/frontend-vue/app/build/components/FormRows.vue +26 -0
  255. package/frontend-vue/app/build/components/RequiredLegend.vue +11 -0
  256. package/frontend-vue/app/components/CategoriesInput.vue +323 -77
  257. package/frontend-vue/app/components/CategoryInput.vue +13 -14
  258. package/frontend-vue/app/components/CategoryLabel.vue +34 -0
  259. package/frontend-vue/app/components/DuplicateModal.vue +201 -0
  260. package/frontend-vue/app/components/Editor.vue +55 -20
  261. package/frontend-vue/app/components/FixAddressModal.vue +133 -0
  262. package/frontend-vue/app/components/FunnelCard.vue +146 -43
  263. package/frontend-vue/app/components/LocaleSelect.vue +98 -0
  264. package/frontend-vue/app/components/NewTemplateModal.vue +388 -102
  265. package/frontend-vue/app/components/NewTemplateSourceTile.vue +70 -0
  266. package/frontend-vue/app/components/OverviewGate.vue +220 -0
  267. package/frontend-vue/app/components/PageHeader.vue +98 -0
  268. package/frontend-vue/app/components/RealSendModal.vue +555 -0
  269. package/frontend-vue/app/components/RecipientsField.vue +41 -0
  270. package/frontend-vue/app/components/RenderingModal.vue +189 -0
  271. package/frontend-vue/app/components/RetentionInput.vue +212 -0
  272. package/frontend-vue/app/components/SendDrawer.vue +542 -209
  273. package/frontend-vue/app/components/StarterGallery.vue +153 -0
  274. package/frontend-vue/app/components/SystemVariables.vue +63 -0
  275. package/frontend-vue/app/components/TemplateCard.vue +218 -57
  276. package/frontend-vue/app/components/TemplateDrawer.vue +785 -274
  277. package/frontend-vue/app/components/TemplateGallery.vue +312 -56
  278. package/frontend-vue/app/components/TemplatePreviewFrame.vue +112 -22
  279. package/frontend-vue/app/components/TestSendModal.vue +266 -95
  280. package/frontend-vue/app/components/TokenText.vue +61 -10
  281. package/frontend-vue/app/components/VariablePath.vue +20 -0
  282. package/frontend-vue/app/components/VersionHistory.vue +196 -0
  283. package/frontend-vue/app/components/VersionPreview.vue +164 -0
  284. package/frontend-vue/app/components/WebhookStatus.vue +130 -0
  285. package/frontend-vue/app/components/blocks/Button.vue +1 -1
  286. package/frontend-vue/app/components/blocks/Code.vue +1 -1
  287. package/frontend-vue/app/components/blocks/Footer.vue +56 -7
  288. package/frontend-vue/app/components/blocks/Heading.vue +1 -1
  289. package/frontend-vue/app/components/blocks/Hero.vue +13 -7
  290. package/frontend-vue/app/components/blocks/List.vue +50 -6
  291. package/frontend-vue/app/components/blocks/Paragraph.vue +1 -1
  292. package/frontend-vue/app/components/blocks/Total.vue +3 -3
  293. package/frontend-vue/app/components/editor/EditorAutocompleteMenu.vue +50 -0
  294. package/frontend-vue/app/components/editor/EditorBlock.vue +37 -90
  295. package/frontend-vue/app/components/editor/EditorBlockFields.vue +140 -0
  296. package/frontend-vue/app/components/editor/EditorBlockInspector.vue +92 -0
  297. package/frontend-vue/app/components/editor/EditorBlockList.vue +32 -25
  298. package/frontend-vue/app/components/editor/EditorBlockPicker.vue +58 -0
  299. package/frontend-vue/app/components/editor/EditorBlockTools.vue +71 -0
  300. package/frontend-vue/app/components/editor/EditorCanvas.vue +105 -70
  301. package/frontend-vue/app/components/editor/EditorCompare.vue +211 -0
  302. package/frontend-vue/app/components/editor/EditorConditionFields.vue +56 -46
  303. package/frontend-vue/app/components/editor/EditorConditionInspector.vue +54 -0
  304. package/frontend-vue/app/components/editor/EditorEnvelope.vue +77 -0
  305. package/frontend-vue/app/components/editor/EditorIfBlock.vue +36 -86
  306. package/frontend-vue/app/components/editor/EditorInsertPoint.vue +64 -0
  307. package/frontend-vue/app/components/editor/EditorInspector.vue +80 -0
  308. package/frontend-vue/app/components/editor/EditorLeftPane.vue +103 -0
  309. package/frontend-vue/app/components/editor/EditorLifecycle.vue +67 -0
  310. package/frontend-vue/app/components/editor/EditorLoading.vue +23 -0
  311. package/frontend-vue/app/components/editor/EditorLocaleSwitch.vue +61 -0
  312. package/frontend-vue/app/components/editor/EditorMissingLocale.vue +72 -48
  313. package/frontend-vue/app/components/editor/EditorMissingPolicy.vue +42 -0
  314. package/frontend-vue/app/components/editor/EditorNotice.vue +127 -0
  315. package/frontend-vue/app/components/editor/EditorOutline.vue +65 -0
  316. package/frontend-vue/app/components/editor/EditorPalette.vue +95 -42
  317. package/frontend-vue/app/components/editor/EditorPaletteGroup.vue +66 -0
  318. package/frontend-vue/app/components/editor/EditorPublishReview.vue +102 -0
  319. package/frontend-vue/app/components/editor/EditorSaveState.vue +88 -0
  320. package/frontend-vue/app/components/editor/EditorShortcutHint.vue +17 -0
  321. package/frontend-vue/app/components/editor/EditorSimChip.vue +51 -0
  322. package/frontend-vue/app/components/editor/EditorTemplatePane.vue +131 -0
  323. package/frontend-vue/app/components/editor/EditorTestDataPane.vue +98 -0
  324. package/frontend-vue/app/components/editor/EditorTitle.vue +82 -0
  325. package/frontend-vue/app/components/editor/EditorTokenField.vue +224 -0
  326. package/frontend-vue/app/components/editor/EditorToolbar.vue +34 -134
  327. package/frontend-vue/app/components/editor/EditorVariableForm.vue +100 -0
  328. package/frontend-vue/app/components/editor/EditorVariableRow.vue +87 -0
  329. package/frontend-vue/app/components/editor/EditorVariablesPane.vue +161 -0
  330. package/frontend-vue/app/components/editor/EditorViewSwitches.vue +76 -0
  331. package/frontend-vue/app/components/editor/EditorVisibility.vue +101 -0
  332. package/frontend-vue/app/components/editor/EditorWorkspace.vue +62 -149
  333. package/frontend-vue/app/composables/useBlockContext.ts +16 -0
  334. package/frontend-vue/app/composables/useEditorActions.ts +381 -0
  335. package/frontend-vue/app/composables/useEditorAnnouncements.ts +51 -0
  336. package/frontend-vue/app/composables/useEditorContext.ts +30 -23
  337. package/frontend-vue/app/composables/useEditorSession.ts +299 -0
  338. package/frontend-vue/app/composables/useEditorShortcuts.ts +81 -0
  339. package/frontend-vue/app/composables/useLocalizedDefaults.ts +17 -0
  340. package/frontend-vue/app/composables/useMailingApi.ts +142 -27
  341. package/frontend-vue/app/composables/useOverviewSetup.ts +13 -0
  342. package/frontend-vue/app/composables/useReplayToast.ts +72 -0
  343. package/frontend-vue/app/composables/useTemplateCategories.ts +35 -0
  344. package/frontend-vue/app/composables/useTemplateDrawer.ts +196 -62
  345. package/frontend-vue/app/composables/useTemplateEditor.ts +318 -86
  346. package/frontend-vue/app/emails/EmailMailingTemplate.vue +16 -4
  347. package/frontend-vue/app/emails/components/MailingBlocks.vue +96 -38
  348. package/frontend-vue/app/emails/components/MailingTextLines.vue +20 -0
  349. package/frontend-vue/app/plugins/column-displays.ts +65 -0
  350. package/frontend-vue/app/plugins/{template-gallery-display.client.ts → template-gallery-display.ts} +1 -1
  351. package/frontend-vue/app/types/mailing.ts +193 -18
  352. package/frontend-vue/app/utils/api-error.ts +33 -0
  353. package/frontend-vue/app/utils/autocomplete.ts +120 -0
  354. package/frontend-vue/app/utils/blocks/components.ts +10 -10
  355. package/frontend-vue/app/utils/blocks/dnd.ts +7 -3
  356. package/frontend-vue/app/utils/blocks/factory.ts +199 -38
  357. package/frontend-vue/app/utils/blocks/fields.ts +39 -19
  358. package/frontend-vue/app/utils/blocks/index.ts +1 -0
  359. package/frontend-vue/app/utils/blocks/outline.ts +91 -0
  360. package/frontend-vue/app/utils/blocks/tree.ts +114 -2
  361. package/frontend-vue/app/utils/categories.ts +201 -15
  362. package/frontend-vue/app/utils/changes.ts +152 -0
  363. package/frontend-vue/app/utils/conditions.ts +25 -19
  364. package/frontend-vue/app/utils/editor-checks.ts +266 -0
  365. package/frontend-vue/app/utils/editor-route.ts +43 -4
  366. package/frontend-vue/app/utils/funnel.ts +74 -0
  367. package/frontend-vue/app/utils/gallery.ts +179 -1
  368. package/frontend-vue/app/utils/history.ts +66 -0
  369. package/frontend-vue/app/utils/lifecycle.ts +185 -0
  370. package/frontend-vue/app/utils/locales.ts +107 -2
  371. package/frontend-vue/app/utils/overview-setup.ts +83 -0
  372. package/frontend-vue/app/utils/pages.ts +5 -0
  373. package/frontend-vue/app/utils/paths.ts +51 -15
  374. package/frontend-vue/app/utils/period.ts +62 -22
  375. package/frontend-vue/app/utils/problems.ts +181 -0
  376. package/frontend-vue/app/utils/recipients.ts +49 -0
  377. package/frontend-vue/app/utils/rendering-title.ts +23 -0
  378. package/frontend-vue/app/utils/send-actions.ts +207 -0
  379. package/frontend-vue/app/utils/send-link.ts +28 -0
  380. package/frontend-vue/app/utils/send-snippet.ts +97 -0
  381. package/frontend-vue/app/utils/send-stats.ts +24 -0
  382. package/frontend-vue/app/utils/send-values.ts +156 -0
  383. package/frontend-vue/app/utils/shortcuts.ts +113 -0
  384. package/frontend-vue/app/utils/slug.ts +82 -0
  385. package/frontend-vue/app/utils/starters.ts +23 -0
  386. package/frontend-vue/app/utils/suppression.ts +112 -0
  387. package/frontend-vue/app/utils/system-variables.ts +165 -0
  388. package/frontend-vue/app/utils/template-attention.ts +104 -0
  389. package/frontend-vue/app/utils/timeline.ts +221 -19
  390. package/frontend-vue/app/utils/tokens.ts +115 -13
  391. package/frontend-vue/app/utils/variable-types.ts +23 -0
  392. package/frontend-vue/app/utils/variables.ts +208 -1
  393. package/frontend-vue/app/utils/version-history.ts +53 -0
  394. package/frontend-vue/app/utils/webhook-status.ts +170 -0
  395. package/frontend-vue/dms.frontend.ts +19 -11
  396. package/frontend-vue/i18n/locales/mailing-en-GB.json +1173 -161
  397. package/frontend-vue/i18n/locales/mailing-fr-FR.json +1220 -208
  398. package/frontend-vue/package.json +2 -2
  399. package/frontend-vue/pnpm-lock.yaml +46 -161
  400. package/frontend-vue/tests/api-error.test.ts +27 -0
  401. package/frontend-vue/tests/autocomplete.test.ts +84 -0
  402. package/frontend-vue/tests/block-components.test.ts +0 -3
  403. package/frontend-vue/tests/block-fields.test.ts +12 -2
  404. package/frontend-vue/tests/blocks-factory.test.ts +46 -0
  405. package/frontend-vue/tests/blocks-outline.test.ts +102 -0
  406. package/frontend-vue/tests/blocks-tree.test.ts +28 -1
  407. package/frontend-vue/tests/categories-input.test.ts +140 -0
  408. package/frontend-vue/tests/categories.test.ts +148 -15
  409. package/frontend-vue/tests/changes.test.ts +136 -0
  410. package/frontend-vue/tests/column-displays.test.ts +73 -0
  411. package/frontend-vue/tests/conditions.test.ts +49 -21
  412. package/frontend-vue/tests/dnd.test.ts +10 -0
  413. package/frontend-vue/tests/editor-block-list.test.ts +25 -2
  414. package/frontend-vue/tests/editor-checks.test.ts +178 -0
  415. package/frontend-vue/tests/editor-route.test.ts +37 -9
  416. package/frontend-vue/tests/editor-session.test.ts +207 -0
  417. package/frontend-vue/tests/footer-block.test.ts +89 -0
  418. package/frontend-vue/tests/funnel-card.test.ts +117 -0
  419. package/frontend-vue/tests/funnel.test.ts +81 -0
  420. package/frontend-vue/tests/gallery.test.ts +124 -2
  421. package/frontend-vue/tests/helpers/nuxt-globals.ts +232 -0
  422. package/frontend-vue/tests/history.test.ts +48 -0
  423. package/frontend-vue/tests/i18n-editor.test.ts +37 -8
  424. package/frontend-vue/tests/i18n.test.ts +55 -0
  425. package/frontend-vue/tests/lifecycle.test.ts +145 -0
  426. package/frontend-vue/tests/locales.test.ts +60 -0
  427. package/frontend-vue/tests/localized-defaults.test.ts +54 -0
  428. package/frontend-vue/tests/new-template-modal.test.ts +211 -0
  429. package/frontend-vue/tests/overview-gate.test.ts +147 -0
  430. package/frontend-vue/tests/overview-setup.test.ts +78 -0
  431. package/frontend-vue/tests/paths.test.ts +23 -0
  432. package/frontend-vue/tests/period.test.ts +59 -10
  433. package/frontend-vue/tests/problems.test.ts +153 -0
  434. package/frontend-vue/tests/recipients.test.ts +47 -0
  435. package/frontend-vue/tests/rendering-title.test.ts +24 -0
  436. package/frontend-vue/tests/replay-toast.test.ts +67 -0
  437. package/frontend-vue/tests/retention-input.test.ts +130 -0
  438. package/frontend-vue/tests/send-actions.test.ts +128 -0
  439. package/frontend-vue/tests/send-modals.test.ts +416 -0
  440. package/frontend-vue/tests/send-snippet.test.ts +51 -0
  441. package/frontend-vue/tests/send-stats.test.ts +12 -0
  442. package/frontend-vue/tests/send-values.test.ts +119 -0
  443. package/frontend-vue/tests/shortcuts.test.ts +76 -0
  444. package/frontend-vue/tests/slug.test.ts +88 -0
  445. package/frontend-vue/tests/suppression.test.ts +159 -0
  446. package/frontend-vue/tests/system-variables.test.ts +133 -0
  447. package/frontend-vue/tests/template-attention.test.ts +123 -0
  448. package/frontend-vue/tests/template-drawer.test.ts +450 -0
  449. package/frontend-vue/tests/template-editor.test.ts +218 -9
  450. package/frontend-vue/tests/template-gallery-display.test.ts +32 -0
  451. package/frontend-vue/tests/template-gallery.test.ts +371 -0
  452. package/frontend-vue/tests/template-overlays.test.ts +125 -0
  453. package/frontend-vue/tests/timeline.test.ts +175 -20
  454. package/frontend-vue/tests/tokens.test.ts +79 -1
  455. package/frontend-vue/tests/variable-path.test.ts +23 -0
  456. package/frontend-vue/tests/variable-types.test.ts +20 -0
  457. package/frontend-vue/tests/variables.test.ts +90 -2
  458. package/frontend-vue/tests/version-history.test.ts +41 -0
  459. package/frontend-vue/tests/webhook-status.test.ts +108 -0
  460. package/frontend-vue/vitest.config.ts +4 -1
  461. package/package.json +14 -12
  462. package/src/automation/email-event-trigger.ts +3 -3
  463. package/src/automation/send-template-action.ts +7 -5
  464. package/src/config.ts +3 -1
  465. package/src/constants.ts +18 -2
  466. package/src/crons/attention.ts +21 -0
  467. package/src/crons/index.ts +2 -0
  468. package/src/crons/jobs.ts +35 -0
  469. package/src/crons/retention.ts +7 -22
  470. package/src/data/displays.ts +18 -0
  471. package/src/data/index.ts +1 -0
  472. package/src/data/locale-fallback.ts +26 -0
  473. package/src/data/read-only-routes.ts +14 -0
  474. package/src/data/sends-table.ts +204 -19
  475. package/src/data/suppressions-table.ts +127 -0
  476. package/src/data/templates-table.ts +295 -63
  477. package/src/data-types/categories-type.ts +4 -5
  478. package/src/data-types/category-type.ts +1 -1
  479. package/src/db/models/index.ts +3 -0
  480. package/src/db/models/sends.model.ts +102 -1
  481. package/src/db/models/settings.model.ts +2 -12
  482. package/src/db/models/suppressions.model.ts +53 -0
  483. package/src/db/models/template-versions.model.ts +23 -0
  484. package/src/db/models/templates.model.ts +17 -0
  485. package/src/db/models/webhook-status.model.ts +25 -0
  486. package/src/db/tables/index.ts +3 -0
  487. package/src/db/tables/send-events.table.ts +6 -2
  488. package/src/db/tables/sends.table.ts +37 -2
  489. package/src/db/tables/suppressions.table.ts +46 -0
  490. package/src/db/tables/template-versions.table.ts +44 -0
  491. package/src/db/tables/templates.table.ts +67 -5
  492. package/src/db/tables/webhook-status.table.ts +44 -0
  493. package/src/engine/changes.ts +198 -0
  494. package/src/engine/conditions.ts +1 -7
  495. package/src/engine/coverage.ts +7 -2
  496. package/src/engine/fallback-html.ts +80 -42
  497. package/src/engine/index.ts +3 -0
  498. package/src/engine/plain-text.ts +86 -0
  499. package/src/engine/resolve.ts +49 -15
  500. package/src/engine/system-variables.ts +124 -0
  501. package/src/engine/tokens.ts +13 -1
  502. package/src/hooks/tenant-export.ts +21 -2
  503. package/src/implementations/dms-mailing/index.ts +1 -1
  504. package/src/index.ts +29 -13
  505. package/src/pages/editor.ts +13 -2
  506. package/src/pages/index.ts +6 -3
  507. package/src/pages/module.ts +31 -2
  508. package/src/pages/overview.ts +187 -128
  509. package/src/pages/sends.ts +271 -112
  510. package/src/pages/settings/form.ts +144 -54
  511. package/src/pages/settings/page.ts +9 -17
  512. package/src/pages/suppressions.ts +157 -0
  513. package/src/pages/templates.ts +140 -43
  514. package/src/permissions.ts +6 -4
  515. package/src/quick-actions.ts +12 -5
  516. package/src/routes/events.ts +110 -33
  517. package/src/routes/index.ts +2 -0
  518. package/src/routes/metrics.ts +348 -98
  519. package/src/routes/period.ts +23 -6
  520. package/src/routes/sends.ts +413 -41
  521. package/src/routes/settings.ts +291 -26
  522. package/src/routes/suppressions.ts +87 -0
  523. package/src/routes/template-versions.ts +116 -0
  524. package/src/routes/templates.ts +334 -48
  525. package/src/services/attention.ts +283 -30
  526. package/src/services/catalog.ts +28 -0
  527. package/src/services/categories.ts +28 -27
  528. package/src/services/checked-send.ts +123 -0
  529. package/src/services/events.ts +174 -20
  530. package/src/services/metrics.ts +137 -28
  531. package/src/services/notify.ts +126 -44
  532. package/src/services/provider-banner.ts +165 -0
  533. package/src/services/render.ts +1 -0
  534. package/src/services/send-stat-items.ts +160 -0
  535. package/src/services/send-stats.ts +122 -0
  536. package/src/services/send.ts +193 -31
  537. package/src/services/settings.ts +52 -8
  538. package/src/services/starters.ts +383 -0
  539. package/src/services/suppressions.ts +116 -0
  540. package/src/services/template-attention.ts +82 -0
  541. package/src/services/template-stats.ts +97 -0
  542. package/src/services/templates.ts +400 -37
  543. package/src/services/webhook-status.ts +202 -0
  544. package/src/types/blocks.ts +6 -0
  545. package/src/types/index.ts +2 -0
  546. package/src/types/sends.ts +64 -2
  547. package/src/types/suppressions.ts +27 -0
  548. package/src/types/webhook.ts +29 -0
  549. package/src/validation/settings.schema.ts +173 -17
  550. package/src/validation/templates.schema.ts +53 -1
  551. package/dist/antelope-modules.d.ts +0 -32
  552. package/dist/antelope-modules.js +0 -36
  553. package/dist/antelope-modules.js.map +0 -1
  554. package/dist/test/antelope.test.d.ts +0 -2
  555. package/dist/test/antelope.test.js +0 -180
  556. package/dist/test/antelope.test.js.map +0 -1
  557. package/dist/test/helpers/http.d.ts +0 -4
  558. package/dist/test/helpers/http.js +0 -28
  559. package/dist/test/helpers/http.js.map +0 -1
  560. package/dist/test/helpers/owner.d.ts +0 -5
  561. package/dist/test/helpers/owner.js +0 -30
  562. package/dist/test/helpers/owner.js.map +0 -1
  563. package/dist/test/integration/events.test.d.ts +0 -1
  564. package/dist/test/integration/events.test.js +0 -143
  565. package/dist/test/integration/events.test.js.map +0 -1
  566. package/dist/test/integration/retention.test.d.ts +0 -1
  567. package/dist/test/integration/retention.test.js +0 -291
  568. package/dist/test/integration/retention.test.js.map +0 -1
  569. package/dist/test/integration/sends.test.d.ts +0 -1
  570. package/dist/test/integration/sends.test.js +0 -210
  571. package/dist/test/integration/sends.test.js.map +0 -1
  572. package/dist/test/integration/settings.test.d.ts +0 -1
  573. package/dist/test/integration/settings.test.js +0 -137
  574. package/dist/test/integration/settings.test.js.map +0 -1
  575. package/dist/test/integration/setup.test.d.ts +0 -1
  576. package/dist/test/integration/setup.test.js +0 -32
  577. package/dist/test/integration/setup.test.js.map +0 -1
  578. package/dist/test/integration/templates.test.d.ts +0 -1
  579. package/dist/test/integration/templates.test.js +0 -233
  580. package/dist/test/integration/templates.test.js.map +0 -1
  581. package/dist/test/integration/tenant-export.test.d.ts +0 -1
  582. package/dist/test/integration/tenant-export.test.js +0 -85
  583. package/dist/test/integration/tenant-export.test.js.map +0 -1
  584. package/dist/test/unit/attention.test.d.ts +0 -1
  585. package/dist/test/unit/attention.test.js +0 -109
  586. package/dist/test/unit/attention.test.js.map +0 -1
  587. package/dist/test/unit/automation.test.d.ts +0 -1
  588. package/dist/test/unit/automation.test.js +0 -104
  589. package/dist/test/unit/automation.test.js.map +0 -1
  590. package/dist/test/unit/categories.test.d.ts +0 -1
  591. package/dist/test/unit/categories.test.js +0 -38
  592. package/dist/test/unit/categories.test.js.map +0 -1
  593. package/dist/test/unit/engine/conditions.test.d.ts +0 -1
  594. package/dist/test/unit/engine/conditions.test.js +0 -23
  595. package/dist/test/unit/engine/conditions.test.js.map +0 -1
  596. package/dist/test/unit/engine/coverage.test.d.ts +0 -1
  597. package/dist/test/unit/engine/coverage.test.js +0 -106
  598. package/dist/test/unit/engine/coverage.test.js.map +0 -1
  599. package/dist/test/unit/engine/fallback-html.test.d.ts +0 -1
  600. package/dist/test/unit/engine/fallback-html.test.js +0 -78
  601. package/dist/test/unit/engine/fallback-html.test.js.map +0 -1
  602. package/dist/test/unit/engine/paths.test.d.ts +0 -1
  603. package/dist/test/unit/engine/paths.test.js +0 -33
  604. package/dist/test/unit/engine/paths.test.js.map +0 -1
  605. package/dist/test/unit/engine/resolve.test.d.ts +0 -1
  606. package/dist/test/unit/engine/resolve.test.js +0 -157
  607. package/dist/test/unit/engine/resolve.test.js.map +0 -1
  608. package/dist/test/unit/engine/tokens.test.d.ts +0 -1
  609. package/dist/test/unit/engine/tokens.test.js +0 -26
  610. package/dist/test/unit/engine/tokens.test.js.map +0 -1
  611. package/dist/test/unit/events.test.d.ts +0 -1
  612. package/dist/test/unit/events.test.js +0 -13
  613. package/dist/test/unit/events.test.js.map +0 -1
  614. package/dist/test/unit/metrics.test.d.ts +0 -1
  615. package/dist/test/unit/metrics.test.js +0 -108
  616. package/dist/test/unit/metrics.test.js.map +0 -1
  617. package/dist/test/unit/notify.test.d.ts +0 -1
  618. package/dist/test/unit/notify.test.js +0 -49
  619. package/dist/test/unit/notify.test.js.map +0 -1
  620. package/dist/test/unit/period.test.d.ts +0 -1
  621. package/dist/test/unit/period.test.js +0 -61
  622. package/dist/test/unit/period.test.js.map +0 -1
  623. package/dist/test/unit/permissions.test.d.ts +0 -1
  624. package/dist/test/unit/permissions.test.js +0 -27
  625. package/dist/test/unit/permissions.test.js.map +0 -1
  626. package/dist/test/unit/provider.test.d.ts +0 -1
  627. package/dist/test/unit/provider.test.js +0 -47
  628. package/dist/test/unit/provider.test.js.map +0 -1
  629. package/dist/test/unit/realtime.test.d.ts +0 -1
  630. package/dist/test/unit/realtime.test.js +0 -13
  631. package/dist/test/unit/realtime.test.js.map +0 -1
  632. package/dist/test/unit/render.test.d.ts +0 -1
  633. package/dist/test/unit/render.test.js +0 -23
  634. package/dist/test/unit/render.test.js.map +0 -1
  635. package/dist/test/unit/retention.test.d.ts +0 -1
  636. package/dist/test/unit/retention.test.js +0 -66
  637. package/dist/test/unit/retention.test.js.map +0 -1
  638. package/dist/test/unit/send-batch.test.d.ts +0 -1
  639. package/dist/test/unit/send-batch.test.js +0 -46
  640. package/dist/test/unit/send-batch.test.js.map +0 -1
  641. package/dist/test/unit/send.test.d.ts +0 -1
  642. package/dist/test/unit/send.test.js +0 -120
  643. package/dist/test/unit/send.test.js.map +0 -1
  644. package/dist/test/unit/smoke.test.d.ts +0 -1
  645. package/dist/test/unit/smoke.test.js +0 -10
  646. package/dist/test/unit/smoke.test.js.map +0 -1
  647. package/dist/test/unit/status.test.d.ts +0 -1
  648. package/dist/test/unit/status.test.js +0 -16
  649. package/dist/test/unit/status.test.js.map +0 -1
  650. package/dist/test/unit/templates.test.d.ts +0 -1
  651. package/dist/test/unit/templates.test.js +0 -48
  652. package/dist/test/unit/templates.test.js.map +0 -1
  653. package/dist/test/unit/tenant-export.test.d.ts +0 -1
  654. package/dist/test/unit/tenant-export.test.js +0 -59
  655. package/dist/test/unit/tenant-export.test.js.map +0 -1
  656. package/dist/test/unit/webhook-secret.test.d.ts +0 -1
  657. package/dist/test/unit/webhook-secret.test.js +0 -43
  658. package/dist/test/unit/webhook-secret.test.js.map +0 -1
  659. package/frontend-vue/app/components/AttentionCard.vue +0 -78
  660. package/frontend-vue/app/components/HtmlPreviewModal.vue +0 -20
  661. package/frontend-vue/app/components/ProviderChip.vue +0 -39
  662. package/frontend-vue/app/components/editor/EditorBlockSettings.vue +0 -186
  663. package/frontend-vue/app/components/editor/EditorDataPanel.vue +0 -130
  664. package/frontend-vue/app/components/editor/EditorDraftBar.vue +0 -35
  665. package/frontend-vue/app/components/editor/EditorRail.vue +0 -58
  666. package/frontend-vue/app/components/editor/EditorSubjectSettings.vue +0 -37
  667. package/frontend-vue/app/components/editor/EditorTemplateSettings.vue +0 -87
  668. package/frontend-vue/app/components/editor/EditorTestSendModal.vue +0 -66
  669. package/frontend-vue/app/components/editor/EditorVariables.vue +0 -163
  670. package/frontend-vue/app/composables/useMailingPeriodData.ts +0 -39
  671. package/frontend-vue/tests/smoke.test.ts +0 -8
  672. package/src/antelope-modules.ts +0 -48
  673. package/src/test/antelope.test.ts +0 -190
  674. package/src/test/helpers/http.ts +0 -23
  675. package/src/test/helpers/owner.ts +0 -37
  676. package/src/test/integration/events.test.ts +0 -159
  677. package/src/test/integration/retention.test.ts +0 -363
  678. package/src/test/integration/sends.test.ts +0 -233
  679. package/src/test/integration/settings.test.ts +0 -128
  680. package/src/test/integration/setup.test.ts +0 -35
  681. package/src/test/integration/templates.test.ts +0 -272
  682. package/src/test/integration/tenant-export.test.ts +0 -125
  683. package/src/test/unit/attention.test.ts +0 -129
  684. package/src/test/unit/automation.test.ts +0 -130
  685. package/src/test/unit/categories.test.ts +0 -51
  686. package/src/test/unit/engine/conditions.test.ts +0 -35
  687. package/src/test/unit/engine/coverage.test.ts +0 -118
  688. package/src/test/unit/engine/fallback-html.test.ts +0 -86
  689. package/src/test/unit/engine/paths.test.ts +0 -39
  690. package/src/test/unit/engine/resolve.test.ts +0 -176
  691. package/src/test/unit/engine/tokens.test.ts +0 -26
  692. package/src/test/unit/events.test.ts +0 -19
  693. package/src/test/unit/metrics.test.ts +0 -170
  694. package/src/test/unit/notify.test.ts +0 -71
  695. package/src/test/unit/period.test.ts +0 -91
  696. package/src/test/unit/permissions.test.ts +0 -30
  697. package/src/test/unit/provider.test.ts +0 -57
  698. package/src/test/unit/realtime.test.ts +0 -11
  699. package/src/test/unit/render.test.ts +0 -23
  700. package/src/test/unit/retention.test.ts +0 -91
  701. package/src/test/unit/send-batch.test.ts +0 -72
  702. package/src/test/unit/send.test.ts +0 -161
  703. package/src/test/unit/smoke.test.ts +0 -8
  704. package/src/test/unit/status.test.ts +0 -14
  705. package/src/test/unit/templates.test.ts +0 -52
  706. package/src/test/unit/tenant-export.test.ts +0 -88
  707. package/src/test/unit/webhook-secret.test.ts +0 -51
@@ -35,7 +35,8 @@ const COLLECTORS: Record<Block["type"], PathCollector> = {
35
35
  const isUserPath = (path: string): boolean =>
36
36
  !SYSTEM_VARIABLE_PATHS.includes(path);
37
37
 
38
- export function collectVariablePaths(content: LocaleContent): string[] {
38
+ /** Every path the content references, the system ones included. */
39
+ export function collectReferencedPaths(content: LocaleContent): string[] {
39
40
  const paths = new Set<string>(textTokens(content.subject, content.preheader));
40
41
  walkBlocks(content.blocks, (block) => {
41
42
  for (const path of COLLECTORS[block.type](block)) {
@@ -43,7 +44,11 @@ export function collectVariablePaths(content: LocaleContent): string[] {
43
44
  }
44
45
  if (block.visibleIf) paths.add(normalizePath(block.visibleIf.path));
45
46
  });
46
- return [...paths].filter(isUserPath).sort();
47
+ return [...paths].sort();
48
+ }
49
+
50
+ export function collectVariablePaths(content: LocaleContent): string[] {
51
+ return collectReferencedPaths(content).filter(isUserPath);
47
52
  }
48
53
 
49
54
  /** Every variable path the content references, across all of its locales. */
@@ -11,6 +11,12 @@ const ESCAPES: Record<string, string> = {
11
11
  export const escapeHtml = (value: string): string =>
12
12
  value.replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
13
13
 
14
+ const LINE_BREAK = /\r?\n/g;
15
+
16
+ /** Escaped text whose line breaks survive as `<br>`, as typed in the editor. */
17
+ const escapeLines = (value: string): string =>
18
+ escapeHtml(value).replace(LINE_BREAK, "<br>");
19
+
14
20
  const SAFE_SCHEMES = ["http:", "https:", "mailto:"];
15
21
  const BLOCKED_HREF = "#";
16
22
  const SCHEME_PATTERN = /^\s*([a-z][a-z0-9+.-]*):/i;
@@ -19,56 +25,90 @@ const SCHEME_PATTERN = /^\s*([a-z][a-z0-9+.-]*):/i;
19
25
  * A link target can be an interpolated variable, so it is data the sender does
20
26
  * not control. Escaping the entities keeps it inside the attribute; this keeps
21
27
  * the scheme to one a mail client may follow. A target with no scheme is
22
- * relative (or still an unresolved token) and passes through.
28
+ * relative (or still an unresolved token) and passes through; a blocked one
29
+ * becomes `blocked`.
23
30
  */
24
- export const safeUrl = (value: string): string => {
31
+ export const safeUrl = (
32
+ value: string,
33
+ blocked: string = BLOCKED_HREF,
34
+ ): string => {
25
35
  const scheme = SCHEME_PATTERN.exec(value);
26
36
  if (!scheme) return value;
27
37
  return SAFE_SCHEMES.includes(scheme[1]!.toLowerCase() + ":")
28
38
  ? value
29
- : BLOCKED_HREF;
39
+ : blocked;
30
40
  };
31
41
 
42
+ /**
43
+ * The font, palette and spacing below are those of the DMS e-mail layout
44
+ * (`emailStyles` of the dms-layout), so an e-mail looks the same whichever
45
+ * renderer drew it.
46
+ */
32
47
  const FONT =
33
- "font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif";
34
- const TEXT_COLOR = "#4c545c";
35
- const HEADING_COLOR = "#16181b";
36
- const BORDER_COLOR = "#e7e9ec";
37
- const MUTED_COLOR = "#98a0a8";
38
- const SURFACE_COLOR = "#f7f8fa";
39
- const BUTTON_STYLE =
40
- "display:inline-block;padding:12px 22px;border-radius:6px;background:#111417;color:#ffffff;text-decoration:none;font-weight:600";
48
+ "font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif";
49
+ const WRAP = "overflow-wrap:anywhere;word-break:break-word";
50
+ const TEXT_COLOR = "#525252";
51
+ const HEADING_COLOR = "#1a1a1a";
52
+ const BORDER_COLOR = "#e4e4e7";
53
+ const MUTED_COLOR = "#71717a";
54
+ const PAGE_COLOR = "#f6f9fc";
55
+ const CODE_SURFACE = "#f4f4f5";
56
+ const CODE_COLOR = "#18181b";
57
+ const CODE_FONT = "font-family:'SF Mono',Monaco,'Courier New',monospace";
58
+ const TEXT_STYLE = `${FONT};font-size:15px;line-height:1.6;color:${TEXT_COLOR};${WRAP}`;
59
+ const BLOCK_SPACING = "margin:0 0 16px";
60
+ const BUTTON_STYLE = `display:inline-block;padding:12px 24px;border-radius:6px;background:${CODE_COLOR};color:#ffffff;text-decoration:none;font-size:15px;font-weight:600`;
61
+ const DIVIDER = `<hr style="border:0;border-top:1px solid ${BORDER_COLOR};margin:32px 0">`;
62
+ const FOOTER_STYLE = `${FONT};font-size:12px;line-height:1.5;color:${MUTED_COLOR};text-align:center;margin:0;${WRAP}`;
63
+ const FOOTER_LINK_SEPARATOR = " · ";
41
64
  const CONTENT_WIDTH = 600;
65
+ const VIEWPORT = "width=device-width, initial-scale=1";
42
66
 
43
67
  type BlockHtml = (block: ResolvedBlock) => string;
44
68
 
45
69
  const link = (label: string, href: string): string =>
46
70
  href
47
- ? `<a href="${escapeHtml(safeUrl(href))}" style="color:${TEXT_COLOR}">${escapeHtml(label)}</a>`
71
+ ? `<a href="${escapeHtml(safeUrl(href))}" style="color:${MUTED_COLOR};text-decoration:underline">${escapeHtml(label)}</a>`
48
72
  : "";
49
73
 
50
- const cellStyle = (weight: string, color: string): string =>
51
- `${FONT};font-size:13px;font-weight:${weight};padding:11px 14px;color:${color}`;
74
+ const row = (label: string, value: string, weight: string): string =>
75
+ `<tr><td style="${TEXT_STYLE};font-weight:${weight}">${escapeHtml(label)}</td><td align="right" style="${TEXT_STYLE};font-weight:${weight};text-align:right">${escapeHtml(value)}</td></tr>`;
76
+
77
+ const table = (rows: string): string =>
78
+ `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="${BLOCK_SPACING}">${rows}</table>`;
52
79
 
53
80
  const TEXT_RENDERERS: Record<string, BlockHtml> = {
54
81
  heading: (block) =>
55
82
  block.type === "heading"
56
- ? `<h1 style="${FONT};font-size:${block.size}px;color:${HEADING_COLOR};text-align:${block.align};margin:0 0 12px">${escapeHtml(block.text)}</h1>`
83
+ ? `<h1 style="${FONT};font-size:${block.size}px;font-weight:600;color:${HEADING_COLOR};text-align:${block.align};margin:0 0 24px;${WRAP}">${escapeLines(block.text)}</h1>`
57
84
  : "",
58
85
  paragraph: (block) =>
59
86
  block.type === "paragraph"
60
- ? `<p style="${FONT};font-size:14px;line-height:1.6;color:${TEXT_COLOR};text-align:${block.align};margin:0 0 18px">${escapeHtml(block.text)}</p>`
87
+ ? `<p style="${TEXT_STYLE};text-align:${block.align};${BLOCK_SPACING}">${escapeLines(block.text)}</p>`
61
88
  : "",
62
89
  code: (block) =>
63
90
  block.type === "code"
64
- ? `<div style="${FONT};font-size:27px;letter-spacing:.32em;text-align:center;padding:18px;border:1px dashed ${BORDER_COLOR};border-radius:8px;margin:0 0 16px">${escapeHtml(block.text)}</div>`
91
+ ? `<div style="background:${CODE_SURFACE};border-radius:6px;margin:24px 0;padding:20px;text-align:center"><p style="${CODE_FONT};font-size:32px;font-weight:700;letter-spacing:6px;color:${CODE_COLOR};margin:0;${WRAP}">${escapeHtml(block.text)}</p></div>`
65
92
  : "",
66
93
  button: (block) =>
67
94
  block.type === "button"
68
- ? `<div style="text-align:${block.align};margin:0 0 18px"><a href="${escapeHtml(safeUrl(block.href))}" style="${FONT};${BUTTON_STYLE}">${escapeHtml(block.text)}</a></div>`
95
+ ? `<div style="text-align:${block.align};margin:24px 0"><a href="${escapeHtml(safeUrl(block.href))}" style="${FONT};${BUTTON_STYLE}">${escapeHtml(block.text)}</a></div>`
69
96
  : "",
70
97
  };
71
98
 
99
+ const footerHtml = (block: ResolvedBlock): string => {
100
+ if (block.type !== "footer") return "";
101
+ const links = [
102
+ link(block.unsubscribeLabel, block.unsubscribeUrl),
103
+ link(block.preferencesLabel, block.preferencesUrl),
104
+ ]
105
+ .filter(Boolean)
106
+ .join(FOOTER_LINK_SEPARATOR);
107
+ if (!block.text.trim() && !links) return "";
108
+ const linkLine = links ? `<br>${links}` : "";
109
+ return `${DIVIDER}<p style="${FOOTER_STYLE}">${escapeLines(block.text)}${linkLine}</p>`;
110
+ };
111
+
72
112
  const LAYOUT_RENDERERS: Record<string, BlockHtml> = {
73
113
  hero: (block) =>
74
114
  block.type === "hero" && block.imageUrl
@@ -76,28 +116,14 @@ const LAYOUT_RENDERERS: Record<string, BlockHtml> = {
76
116
  : "",
77
117
  list: (block) =>
78
118
  block.type === "list"
79
- ? `<table role="presentation" width="100%" style="border:1px solid ${BORDER_COLOR};border-radius:8px;margin:0 0 18px">${block.rows
80
- .map(
81
- (row) =>
82
- `<tr><td style="${cellStyle("400", TEXT_COLOR)}">${escapeHtml(row.label)}</td><td align="right" style="${cellStyle("400", HEADING_COLOR)}">${escapeHtml(row.value)}</td></tr>`,
83
- )
84
- .join("")}</table>`
119
+ ? table(
120
+ block.rows.map((item) => row(item.label, item.value, "400")).join(""),
121
+ )
85
122
  : "",
86
123
  total: (block) =>
87
- block.type === "total"
88
- ? `<table role="presentation" width="100%" style="margin:0 0 18px"><tr><td style="${cellStyle("600", HEADING_COLOR)};background:${SURFACE_COLOR}">${escapeHtml(block.label)}</td><td align="right" style="${cellStyle("600", HEADING_COLOR)};background:${SURFACE_COLOR}">${escapeHtml(block.value)}</td></tr></table>`
89
- : "",
90
- divider: () =>
91
- `<hr style="border:0;border-top:1px solid ${BORDER_COLOR};margin:10px 0 18px">`,
92
- footer: (block) =>
93
- block.type === "footer"
94
- ? `<div style="${FONT};font-size:11px;line-height:1.6;color:${MUTED_COLOR};border-top:1px solid ${BORDER_COLOR};padding:16px 0 0">${escapeHtml(block.text)}<br>${[
95
- link(block.unsubscribeLabel, block.unsubscribeUrl),
96
- link(block.preferencesLabel, block.preferencesUrl),
97
- ]
98
- .filter(Boolean)
99
- .join(" · ")}</div>`
100
- : "",
124
+ block.type === "total" ? table(row(block.label, block.value, "600")) : "",
125
+ divider: () => DIVIDER,
126
+ footer: footerHtml,
101
127
  };
102
128
 
103
129
  const RENDERERS: Record<ResolvedBlock["type"], BlockHtml> = {
@@ -105,12 +131,24 @@ const RENDERERS: Record<ResolvedBlock["type"], BlockHtml> = {
105
131
  ...LAYOUT_RENDERERS,
106
132
  } as Record<ResolvedBlock["type"], BlockHtml>;
107
133
 
134
+ const FOOTER_TYPE = "footer";
135
+
136
+ const renderBlocks = (blocks: ResolvedBlock[]): string =>
137
+ blocks.map((block) => RENDERERS[block.type](block)).join("");
138
+
139
+ /**
140
+ * The e-mail without the DMS front end: the same blocks, in the same order and
141
+ * look as the Vue template, the footer last, under its divider.
142
+ */
108
143
  export function renderFallbackHtml(
109
144
  email: ResolvedEmail,
110
145
  locale: string,
111
146
  ): string {
112
- const body = email.blocks
113
- .map((block) => RENDERERS[block.type](block))
114
- .join("");
115
- return `<!DOCTYPE html><html lang="${escapeHtml(locale)}"><head><meta charset="utf-8"><title>${escapeHtml(email.subject)}</title></head><body style="margin:0;background:${SURFACE_COLOR}"><span style="display:none;max-height:0;overflow:hidden">${escapeHtml(email.preheader)}</span><table role="presentation" width="100%"><tr><td align="center" style="padding:26px"><table role="presentation" width="${CONTENT_WIDTH}" style="max-width:${CONTENT_WIDTH}px;background:#ffffff;border-radius:10px"><tr><td style="padding:26px">${body}</td></tr></table></td></tr></table></body></html>`;
147
+ const body = renderBlocks(
148
+ email.blocks.filter((block) => block.type !== FOOTER_TYPE),
149
+ );
150
+ const footer = renderBlocks(
151
+ email.blocks.filter((block) => block.type === FOOTER_TYPE),
152
+ );
153
+ return `<!DOCTYPE html><html lang="${escapeHtml(locale)}"><head><meta charset="utf-8"><meta name="viewport" content="${VIEWPORT}"><title>${escapeHtml(email.subject)}</title></head><body style="margin:0;background:${PAGE_COLOR};${FONT}"><span style="display:none;max-height:0;overflow:hidden">${escapeHtml(email.preheader)}</span><table role="presentation" width="100%"><tr><td align="center" style="padding:20px 0 48px"><table role="presentation" width="${CONTENT_WIDTH}" style="max-width:${CONTENT_WIDTH}px;width:100%;background:#ffffff;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,0.1)"><tr><td style="padding:40px">${body}${footer}</td></tr></table></td></tr></table></body></html>`;
116
154
  }
@@ -1,7 +1,10 @@
1
+ export * from "./changes";
1
2
  export * from "./conditions";
2
3
  export * from "./coverage";
3
4
  export * from "./fallback-html";
4
5
  export * from "./paths";
6
+ export * from "./plain-text";
5
7
  export * from "./resolve";
8
+ export * from "./system-variables";
6
9
  export * from "./tokens";
7
10
  export * from "./walk";
@@ -0,0 +1,86 @@
1
+ import type { ResolvedBlock, ResolvedEmail } from "../types";
2
+
3
+ type BlockText = (block: ResolvedBlock, separator: string) => string;
4
+
5
+ const PARAGRAPH_BREAK = "\n\n";
6
+ const LINE_BREAK = "\n";
7
+ const DEFAULT_LABEL_SEPARATOR = ": ";
8
+ const LOCALE_PARTS = /[-_]/;
9
+ const DIVIDER = "----------";
10
+ const FOOTER_TYPE = "footer";
11
+
12
+ /**
13
+ * Languages whose typography spaces a colon off its label, with a no-break
14
+ * space so the colon never starts a line ("Total : 64,00 €").
15
+ */
16
+ const LABEL_SEPARATORS: Record<string, string> = {
17
+ fr: " : ",
18
+ };
19
+
20
+ /** What goes between a label and its value in the e-mail's language. */
21
+ export function labelSeparatorOf(locale = ""): string {
22
+ const language = locale.toLowerCase().split(LOCALE_PARTS)[0] ?? "";
23
+ return LABEL_SEPARATORS[language] ?? DEFAULT_LABEL_SEPARATOR;
24
+ }
25
+
26
+ const linked = (label: string, href: string, separator: string): string =>
27
+ href ? `${label}${separator}${href}` : "";
28
+
29
+ const rowsText = (rows: string[][], separator: string): string =>
30
+ rows
31
+ .map((cells) => cells.filter(Boolean).join(separator))
32
+ .filter(Boolean)
33
+ .join(LINE_BREAK);
34
+
35
+ const footerText = (block: ResolvedBlock, separator: string): string => {
36
+ if (block.type !== FOOTER_TYPE) return "";
37
+ const lines = [
38
+ block.text,
39
+ linked(block.unsubscribeLabel, block.unsubscribeUrl, separator),
40
+ linked(block.preferencesLabel, block.preferencesUrl, separator),
41
+ ].filter((line) => line.trim());
42
+ return lines.length ? [DIVIDER, ...lines].join(LINE_BREAK) : "";
43
+ };
44
+
45
+ const BLOCK_TEXT: Record<ResolvedBlock["type"], BlockText> = {
46
+ hero: (block) => (block.type === "hero" && block.imageUrl ? block.alt : ""),
47
+ heading: (block) => (block.type === "heading" ? block.text : ""),
48
+ paragraph: (block) => (block.type === "paragraph" ? block.text : ""),
49
+ code: (block) => (block.type === "code" ? block.text : ""),
50
+ list: (block, separator) =>
51
+ block.type === "list"
52
+ ? rowsText(
53
+ block.rows.map((row) => [row.label, row.value]),
54
+ separator,
55
+ )
56
+ : "",
57
+ total: (block, separator) =>
58
+ block.type === "total"
59
+ ? rowsText([[block.label, block.value]], separator)
60
+ : "",
61
+ button: (block, separator) =>
62
+ block.type === "button" ? linked(block.text, block.href, separator) : "",
63
+ divider: () => DIVIDER,
64
+ footer: footerText,
65
+ };
66
+
67
+ const blocksText = (blocks: ResolvedBlock[], separator: string): string[] =>
68
+ blocks
69
+ .map((block) => BLOCK_TEXT[block.type](block, separator).trim())
70
+ .filter(Boolean);
71
+
72
+ /**
73
+ * The plain-text part of the e-mail: the same blocks as the HTML, in the same
74
+ * order, footer last, punctuated the way the e-mail's language writes it.
75
+ * Mail clients that show no HTML, and spam filters that weigh a missing text
76
+ * part, read this one.
77
+ */
78
+ export function renderPlainText(email: ResolvedEmail, locale = ""): string {
79
+ const separator = labelSeparatorOf(locale);
80
+ const body = email.blocks.filter((block) => block.type !== FOOTER_TYPE);
81
+ const footer = email.blocks.filter((block) => block.type === FOOTER_TYPE);
82
+ return [
83
+ ...blocksText(body, separator),
84
+ ...blocksText(footer, separator),
85
+ ].join(PARAGRAPH_BREAK);
86
+ }
@@ -1,6 +1,11 @@
1
- import { SYSTEM_VARIABLE_PATHS } from "../constants";
1
+ import {
2
+ PREFERENCES_URL_PATH,
3
+ SYSTEM_VARIABLE_PATHS,
4
+ UNSUBSCRIBE_URL_PATH,
5
+ } from "../constants";
2
6
  import type {
3
7
  Block,
8
+ FooterBlock,
4
9
  IfBlock,
5
10
  ListBlock,
6
11
  LocaleContent,
@@ -9,28 +14,51 @@ import type {
9
14
  ResolvedListRow,
10
15
  } from "../types";
11
16
  import { evaluateCondition } from "./conditions";
17
+ import { safeUrl } from "./fallback-html";
12
18
  import { getPath, isPresent } from "./paths";
13
- import { interpolate } from "./tokens";
19
+ import { footerLinkPaths } from "./system-variables";
20
+ import { type InterpolationMode, interpolate } from "./tokens";
14
21
 
15
22
  type Data = Record<string, unknown>;
16
23
 
17
24
  interface ResolveState {
18
25
  data: Data;
26
+ mode: InterpolationMode;
19
27
  missing: Set<string>;
20
28
  hidden: string[];
21
29
  }
22
30
 
31
+ /** A blocked image source: the hero is left out rather than drawn broken. */
32
+ const BLOCKED_IMAGE = "";
33
+
23
34
  type BlockResolver = (block: Block, state: ResolveState) => ResolvedBlock[];
24
35
 
25
36
  function text(state: ResolveState, value: string): string {
26
- const result = interpolate(value, state.data);
37
+ const result = interpolate(value, state.data, state.mode);
27
38
  for (const path of result.missing) state.missing.add(path);
28
39
  return result.text;
29
40
  }
30
41
 
31
- function systemUrl(state: ResolveState, path: string): string {
42
+ /**
43
+ * A link or image target, filtered here so both renderers (the Vue e-mail
44
+ * and the fallback) receive a scheme a mail client may follow.
45
+ */
46
+ function url(state: ResolveState, value: string, blocked?: string): string {
47
+ return safeUrl(text(state, value), blocked);
48
+ }
49
+
50
+ /**
51
+ * A footer link's target, read from the data the caller passed. A link whose
52
+ * label was cleared is left out, whatever URL the caller gave.
53
+ */
54
+ function systemUrl(
55
+ state: ResolveState,
56
+ block: FooterBlock,
57
+ path: string,
58
+ ): string {
59
+ if (!footerLinkPaths(block).includes(path)) return "";
32
60
  const value = getPath(state.data, path);
33
- return isPresent(value) ? String(value) : "";
61
+ return isPresent(value) ? safeUrl(String(value)) : "";
34
62
  }
35
63
 
36
64
  function cell(item: unknown, path: string): string {
@@ -65,7 +93,7 @@ const RESOLVERS: Record<Block["type"], BlockResolver> = {
65
93
  {
66
94
  id: block.id,
67
95
  type: "hero",
68
- imageUrl: text(state, block.imageUrl),
96
+ imageUrl: url(state, block.imageUrl, BLOCKED_IMAGE),
69
97
  alt: text(state, block.alt),
70
98
  },
71
99
  ]
@@ -119,7 +147,7 @@ const RESOLVERS: Record<Block["type"], BlockResolver> = {
119
147
  id: block.id,
120
148
  type: "button",
121
149
  text: text(state, block.text),
122
- href: text(state, block.href),
150
+ href: url(state, block.href),
123
151
  align: block.align,
124
152
  },
125
153
  ]
@@ -135,20 +163,21 @@ const RESOLVERS: Record<Block["type"], BlockResolver> = {
135
163
  text: text(state, block.text),
136
164
  unsubscribeLabel: block.unsubscribeLabel,
137
165
  preferencesLabel: block.preferencesLabel,
138
- unsubscribeUrl: systemUrl(state, "unsubscribeUrl"),
139
- preferencesUrl: systemUrl(state, "preferencesUrl"),
166
+ unsubscribeUrl: systemUrl(state, block, UNSUBSCRIBE_URL_PATH),
167
+ preferencesUrl: systemUrl(state, block, PREFERENCES_URL_PATH),
140
168
  },
141
169
  ]
142
170
  : [],
143
171
  };
144
172
 
173
+ /**
174
+ * Visibility decides before a block is resolved, not after: resolving first
175
+ * would record a hidden block's unresolved paths in `state.missing`, and
176
+ * `blockOnMissingVariables` refuses a send on that set, so a hidden block
177
+ * could block a send over a variable it never rendered.
178
+ */
145
179
  function resolveBlocks(blocks: Block[], state: ResolveState): ResolvedBlock[] {
146
180
  return blocks.flatMap((block) => {
147
- // Visibility decides before the block is resolved, not after. Resolving
148
- // first recorded the block's unresolved paths in `state.missing` even
149
- // though it was then discarded — and `blockOnMissingVariables` refuses a
150
- // send on that set, so a hidden block could block a send over a variable
151
- // it never rendered.
152
181
  if (block.visibleIf && !evaluateCondition(block.visibleIf, state.data)) {
153
182
  state.hidden.push(block.id);
154
183
  return [];
@@ -160,11 +189,16 @@ function resolveBlocks(blocks: Block[], state: ResolveState): ResolvedBlock[] {
160
189
  const isUserPath = (path: string): boolean =>
161
190
  !SYSTEM_VARIABLE_PATHS.includes(path);
162
191
 
192
+ /**
193
+ * Resolves `content` against `data`. A real send passes `send` so a missing
194
+ * value leaves a blank; previews keep the token visible.
195
+ */
163
196
  export function resolveEmail(
164
197
  content: LocaleContent,
165
198
  data: Data,
199
+ mode: InterpolationMode = "preview",
166
200
  ): ResolvedEmail {
167
- const state: ResolveState = { data, missing: new Set(), hidden: [] };
201
+ const state: ResolveState = { data, mode, missing: new Set(), hidden: [] };
168
202
  const subject = text(state, content.subject);
169
203
  const preheader = text(state, content.preheader);
170
204
  const blocks = resolveBlocks(content.blocks, state);
@@ -0,0 +1,124 @@
1
+ import {
2
+ PREFERENCES_URL_PATH,
3
+ SYSTEM_VARIABLE_PATHS,
4
+ UNSUBSCRIBE_URL_PATH,
5
+ } from "../constants";
6
+ import type {
7
+ Block,
8
+ FooterBlock,
9
+ LocaleContent,
10
+ TemplateContent,
11
+ VariableDefinition,
12
+ } from "../types";
13
+ import { collectReferencedPaths } from "./coverage";
14
+ import { getPath, isPresent } from "./paths";
15
+ import { walkBlocks } from "./walk";
16
+
17
+ /**
18
+ * The category whose e-mails are campaigns; every other one is transactional.
19
+ * The settings keep it: it can be renamed, never removed.
20
+ */
21
+ export const MARKETING_CATEGORY = "marketing";
22
+
23
+ const SYSTEM_VARIABLE_TYPE = "url";
24
+
25
+ type FooterLabelKey = "unsubscribeLabel" | "preferencesLabel";
26
+
27
+ /** Each footer link: the label that shows it and the URL the caller passes. */
28
+ const FOOTER_LINKS: Record<FooterLabelKey, string> = {
29
+ unsubscribeLabel: UNSUBSCRIBE_URL_PATH,
30
+ preferencesLabel: PREFERENCES_URL_PATH,
31
+ };
32
+
33
+ const LABEL_KEYS = Object.keys(FOOTER_LINKS) as FooterLabelKey[];
34
+
35
+ export function isMarketingCategory(category?: string | null): boolean {
36
+ return category === MARKETING_CATEGORY;
37
+ }
38
+
39
+ /**
40
+ * Whether a new footer of this category carries the unsubscribe and
41
+ * preferences links: a transactional e-mail (an order, a password reset…)
42
+ * is not subscribed to, so only an uncategorised or marketing one does.
43
+ */
44
+ export function hasFooterLinksByDefault(category?: string | null): boolean {
45
+ return !category || isMarketingCategory(category);
46
+ }
47
+
48
+ const isFooter = (block: Block): block is FooterBlock =>
49
+ block.type === "footer";
50
+
51
+ /** The URL paths a footer shows a link for: the ones with a label. */
52
+ export function footerLinkPaths(block: FooterBlock): string[] {
53
+ return LABEL_KEYS.filter((key) => block[key].trim()).map(
54
+ (key) => FOOTER_LINKS[key],
55
+ );
56
+ }
57
+
58
+ /** The system paths a locale uses: footer links, then `{{…}}` references. */
59
+ export function localeSystemPaths(content: LocaleContent): string[] {
60
+ const paths = new Set<string>();
61
+ walkBlocks(content.blocks, (block) => {
62
+ if (isFooter(block)) footerLinkPaths(block).forEach((p) => paths.add(p));
63
+ });
64
+ collectReferencedPaths(content)
65
+ .filter((path) => SYSTEM_VARIABLE_PATHS.includes(path))
66
+ .forEach((path) => paths.add(path));
67
+ return SYSTEM_VARIABLE_PATHS.filter((path) => paths.has(path));
68
+ }
69
+
70
+ /** The system paths any locale of the content uses. */
71
+ export function contentSystemPaths(content: TemplateContent): string[] {
72
+ const used = new Set(
73
+ Object.values(content.locales).flatMap(localeSystemPaths),
74
+ );
75
+ return SYSTEM_VARIABLE_PATHS.filter((path) => used.has(path));
76
+ }
77
+
78
+ /**
79
+ * The URLs a template's caller passes for the links its content shows,
80
+ * declared like any variable of its contract: a marketing e-mail must carry a
81
+ * working unsubscribe link, so they are required there.
82
+ */
83
+ export function systemVariables(
84
+ paths: string[],
85
+ category?: string | null,
86
+ ): VariableDefinition[] {
87
+ const required = isMarketingCategory(category);
88
+ return paths.map((path) => ({ path, type: SYSTEM_VARIABLE_TYPE, required }));
89
+ }
90
+
91
+ /** The required system URLs `data` does not provide. */
92
+ export function missingSystemVariables(
93
+ content: LocaleContent,
94
+ category: string | null | undefined,
95
+ data: Record<string, unknown>,
96
+ ): string[] {
97
+ return systemVariables(localeSystemPaths(content), category)
98
+ .filter((variable) => variable.required)
99
+ .map((variable) => variable.path)
100
+ .filter((path) => !isPresent(getPath(data, path)));
101
+ }
102
+
103
+ const withoutLinks = (block: Block): Block =>
104
+ isFooter(block)
105
+ ? { ...block, unsubscribeLabel: "", preferencesLabel: "" }
106
+ : block;
107
+
108
+ /**
109
+ * The content with the footers its category calls for: a transactional
110
+ * template loses the unsubscribe and preferences links.
111
+ */
112
+ export function footerForCategory(
113
+ content: TemplateContent,
114
+ category?: string | null,
115
+ ): TemplateContent {
116
+ if (hasFooterLinksByDefault(category)) return content;
117
+ const locales = Object.fromEntries(
118
+ Object.entries(content.locales).map(([code, locale]) => [
119
+ code,
120
+ { ...locale, blocks: locale.blocks.map(withoutLinks) },
121
+ ]),
122
+ );
123
+ return { ...content, locales };
124
+ }
@@ -2,11 +2,22 @@ import { getPath, isPresent } from "./paths";
2
2
 
3
3
  export const TOKEN_PATTERN = /\{\{\s*([A-Za-z0-9_.[\]]+)\s*\}\}/g;
4
4
 
5
+ /**
6
+ * How a token without a value prints. `preview` keeps it visible so an author
7
+ * sees what is missing; `send` blanks it, so a recipient never reads braces.
8
+ */
9
+ export type InterpolationMode = "preview" | "send";
10
+
5
11
  export interface InterpolationResult {
6
12
  text: string;
7
13
  missing: string[];
8
14
  }
9
15
 
16
+ const MISSING_TOKEN: Record<InterpolationMode, (token: string) => string> = {
17
+ preview: (token) => token,
18
+ send: () => "",
19
+ };
20
+
10
21
  export function extractTokens(text: string): string[] {
11
22
  const found = new Set<string>();
12
23
  for (const match of text.matchAll(TOKEN_PATTERN))
@@ -21,13 +32,14 @@ function formatValue(value: unknown): string {
21
32
  export function interpolate(
22
33
  text: string,
23
34
  data: Record<string, unknown>,
35
+ mode: InterpolationMode = "preview",
24
36
  ): InterpolationResult {
25
37
  const missing = new Set<string>();
26
38
  const output = text.replace(TOKEN_PATTERN, (token, path: string) => {
27
39
  const value = getPath(data, path);
28
40
  if (!isPresent(value)) {
29
41
  missing.add(path);
30
- return token;
42
+ return MISSING_TOKEN[mode](token);
31
43
  }
32
44
  return formatValue(value);
33
45
  });
@@ -9,7 +9,12 @@ import {
9
9
  UnregisterHook,
10
10
  } from "@antelopejs/interface-dms/hooks";
11
11
  import { MODULE_ID } from "../constants";
12
- import { SettingsModel, TemplateModel } from "../db";
12
+ import {
13
+ SettingsModel,
14
+ SuppressionModel,
15
+ TemplateModel,
16
+ TemplateVersionModel,
17
+ } from "../db";
13
18
  import type { MailingSettingsValues } from "../types";
14
19
 
15
20
  export type ExportedSettings = Omit<MailingSettingsValues, "webhookSecret">;
@@ -32,7 +37,9 @@ export interface ExportedCollection {
32
37
  export const TENANT_EXPORT_PAGE_SIZE = 200;
33
38
 
34
39
  const TEMPLATES_ENTRY = "templates.json";
40
+ const TEMPLATE_VERSIONS_ENTRY = "template-versions.json";
35
41
  const SETTINGS_ENTRY = "settings.json";
42
+ const SUPPRESSIONS_ENTRY = "suppressions.json";
36
43
 
37
44
  const ARRAY_OPEN = "[";
38
45
  const ARRAY_CLOSE = "]";
@@ -64,7 +71,7 @@ export async function* jsonArrayChunks<T>(
64
71
  async function exportedSettings(
65
72
  tenantId: string,
66
73
  ): Promise<ExportedSettings | null> {
67
- const row = await GetModel(SettingsModel, tenantId).getSingleton(tenantId);
74
+ const row = await GetModel(SettingsModel, tenantId).get(tenantId);
68
75
  if (!row) return null;
69
76
  return {
70
77
  fallbackLocale: row.fallbackLocale,
@@ -93,12 +100,24 @@ async function loadPage<T extends object>(
93
100
 
94
101
  export function exportedCollections(tenantId: string): ExportedCollection[] {
95
102
  const templates = GetModel(TemplateModel, tenantId);
103
+ const versions = GetModel(TemplateVersionModel, tenantId);
104
+ const suppressions = GetModel(SuppressionModel, tenantId);
96
105
  return [
97
106
  {
98
107
  entry: TEMPLATES_ENTRY,
99
108
  load: (offset, size) =>
100
109
  loadPage(templates.table, TemplateModel, offset, size),
101
110
  },
111
+ {
112
+ entry: TEMPLATE_VERSIONS_ENTRY,
113
+ load: (offset, size) =>
114
+ loadPage(versions.table, TemplateVersionModel, offset, size),
115
+ },
116
+ {
117
+ entry: SUPPRESSIONS_ENTRY,
118
+ load: (offset, size) =>
119
+ loadPage(suppressions.table, SuppressionModel, offset, size),
120
+ },
102
121
  ];
103
122
  }
104
123
 
@@ -1,2 +1,2 @@
1
1
  export { recordEmailEvent as RecordEmailEvent } from "../../services/events";
2
- export { sendTemplate as SendTemplate } from "../../services/send";
2
+ export { sendUnlessSuppressed as SendTemplate } from "../../services/checked-send";