@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
@@ -0,0 +1,202 @@
1
+ import { Logging } from "@antelopejs/interface-core/logging";
2
+ import { GetModel } from "@antelopejs/interface-database-decorators";
3
+ import { type MailingWebhookStatus, WebhookStatusModel } from "../db";
4
+ import type {
5
+ SendEventType,
6
+ WebhookReceipt,
7
+ WebhookRefusal,
8
+ WebhookStatus,
9
+ } from "../types";
10
+ import { webhookSecretMatches } from "./events";
11
+
12
+ /** The message id of the event the settings' "Send a test event" posts. */
13
+ export const WEBHOOK_TEST_MESSAGE_ID = "dms-mailing-webhook-test";
14
+
15
+ /** The header a provider (or its relay) proves it knows the secret with. */
16
+ export const WEBHOOK_SECRET_HEADER = "x-mailing-webhook-secret";
17
+
18
+ const WEBHOOK_TEST_EVENT_TYPE: SendEventType = "delivered";
19
+ const WEBHOOK_TEST_TIMEOUT_MS = 10_000;
20
+ const JSON_CONTENT_TYPE = "application/json";
21
+
22
+ /** What the webhook answers a caller it refuses, in its own words. */
23
+ export const WEBHOOK_REFUSAL_MESSAGES: Record<WebhookRefusal, string> = {
24
+ missing_secret: `The ${WEBHOOK_SECRET_HEADER} header is missing. Send the webhook secret shown in Mailing > Settings > Provider & webhook.`,
25
+ bad_secret: `The ${WEBHOOK_SECRET_HEADER} header does not match the webhook secret of this workspace (Mailing > Settings > Provider & webhook). It changes when the secret is rotated.`,
26
+ invalid_event:
27
+ 'The body is not a mailing event: send JSON with "messageId" and "type".',
28
+ };
29
+
30
+ /** The body of a refused webhook call. */
31
+ export interface WebhookRefusalBody {
32
+ error: "webhook_refused";
33
+ reason: WebhookRefusal;
34
+ message: string;
35
+ }
36
+
37
+ /** What "Send a test event" found when it called the webhook URL. */
38
+ export interface WebhookTestOutcome {
39
+ isReceived: boolean;
40
+ url: string;
41
+ durationMs: number;
42
+ /** The HTTP status the URL answered; absent when it could not be reached. */
43
+ httpStatus?: number;
44
+ /** Why the webhook refused the test event. */
45
+ refusal?: WebhookRefusal;
46
+ /** Why the URL could not be reached (`ECONNREFUSED`, a timeout…). */
47
+ networkError?: string;
48
+ }
49
+
50
+ export type WebhookFetch = (
51
+ url: string,
52
+ init: RequestInit,
53
+ ) => Promise<Response>;
54
+
55
+ export function refusalBody(reason: WebhookRefusal): WebhookRefusalBody {
56
+ return {
57
+ error: "webhook_refused",
58
+ reason,
59
+ message: WEBHOOK_REFUSAL_MESSAGES[reason],
60
+ };
61
+ }
62
+
63
+ /** Why a request's secret is refused, `null` when it carries the tenant's. */
64
+ export function secretRefusal(
65
+ provided: string | undefined,
66
+ stored: string | undefined,
67
+ ): WebhookRefusal | null {
68
+ if (!provided) return "missing_secret";
69
+ return webhookSecretMatches(provided, stored) ? null : "bad_secret";
70
+ }
71
+
72
+ export function isWebhookTestEvent(messageId: string): boolean {
73
+ return messageId === WEBHOOK_TEST_MESSAGE_ID;
74
+ }
75
+
76
+ async function writeStatus(
77
+ tenantId: string,
78
+ patch: Partial<MailingWebhookStatus>,
79
+ ): Promise<void> {
80
+ try {
81
+ await GetModel(WebhookStatusModel, tenantId).record(tenantId, patch);
82
+ } catch (error) {
83
+ Logging.Error("[dms-mailing] Could not record the webhook status:", error);
84
+ }
85
+ }
86
+
87
+ /** Remembers the last request the webhook accepted; never throws. */
88
+ export function noteWebhookReceived(
89
+ tenantId: string,
90
+ receipt: Omit<WebhookReceipt, "at">,
91
+ ): Promise<void> {
92
+ return writeStatus(tenantId, {
93
+ receivedAt: new Date(),
94
+ receivedProvider: receipt.provider,
95
+ receivedType: receipt.type,
96
+ isReceivedMatched: receipt.isMatched,
97
+ isReceivedTest: receipt.isTest,
98
+ });
99
+ }
100
+
101
+ /** Remembers the last request the webhook refused; never throws. */
102
+ export function noteWebhookRefused(
103
+ tenantId: string,
104
+ provider: string,
105
+ reason: WebhookRefusal,
106
+ ): Promise<void> {
107
+ return writeStatus(tenantId, {
108
+ refusedAt: new Date(),
109
+ refusedProvider: provider,
110
+ refusedReason: reason,
111
+ });
112
+ }
113
+
114
+ export function toWebhookStatus(
115
+ row: MailingWebhookStatus | undefined,
116
+ ): WebhookStatus {
117
+ return {
118
+ lastReceived: row?.receivedAt
119
+ ? {
120
+ at: row.receivedAt,
121
+ provider: row.receivedProvider ?? "",
122
+ type: row.receivedType ?? "",
123
+ isMatched: !!row.isReceivedMatched,
124
+ isTest: !!row.isReceivedTest,
125
+ }
126
+ : null,
127
+ lastRefused:
128
+ row?.refusedAt && row.refusedReason
129
+ ? {
130
+ at: row.refusedAt,
131
+ provider: row.refusedProvider ?? "",
132
+ reason: row.refusedReason,
133
+ }
134
+ : null,
135
+ };
136
+ }
137
+
138
+ export async function readWebhookStatus(
139
+ tenantId: string,
140
+ ): Promise<WebhookStatus> {
141
+ return toWebhookStatus(
142
+ await GetModel(WebhookStatusModel, tenantId).get(tenantId),
143
+ );
144
+ }
145
+
146
+ const describeError = (error: unknown): string =>
147
+ error instanceof Error
148
+ ? String((error.cause as Error | undefined)?.message ?? error.message)
149
+ : String(error);
150
+
151
+ async function refusalOf(
152
+ response: Response,
153
+ ): Promise<WebhookRefusal | undefined> {
154
+ try {
155
+ const body = (await response.json()) as Partial<WebhookRefusalBody>;
156
+ return body.reason;
157
+ } catch {
158
+ return undefined;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Posts a test event to the webhook URL the settings show, the way a
164
+ * provider would: the URL, the secret and the event format are all tried.
165
+ */
166
+ export async function sendWebhookTest(
167
+ url: string,
168
+ secret: string,
169
+ fetcher: WebhookFetch = fetch,
170
+ ): Promise<WebhookTestOutcome> {
171
+ const startedAt = Date.now();
172
+ const elapsed = () => Date.now() - startedAt;
173
+ try {
174
+ const response = await fetcher(url, {
175
+ method: "POST",
176
+ headers: {
177
+ "content-type": JSON_CONTENT_TYPE,
178
+ [WEBHOOK_SECRET_HEADER]: secret,
179
+ },
180
+ body: JSON.stringify({
181
+ messageId: WEBHOOK_TEST_MESSAGE_ID,
182
+ type: WEBHOOK_TEST_EVENT_TYPE,
183
+ details: { test: true },
184
+ }),
185
+ signal: AbortSignal.timeout(WEBHOOK_TEST_TIMEOUT_MS),
186
+ });
187
+ return {
188
+ isReceived: response.ok,
189
+ url,
190
+ durationMs: elapsed(),
191
+ httpStatus: response.status,
192
+ refusal: response.ok ? undefined : await refusalOf(response),
193
+ };
194
+ } catch (error) {
195
+ return {
196
+ isReceived: false,
197
+ url,
198
+ durationMs: elapsed(),
199
+ networkError: describeError(error),
200
+ };
201
+ }
202
+ }
@@ -29,3 +29,9 @@ export type {
29
29
 
30
30
  /** Internal to the module: templates are never exposed by status. */
31
31
  export type TemplateStatus = "draft" | "live" | "archived";
32
+
33
+ /** Why a template needs a look on the templates page. */
34
+ export type TemplateAttentionReason =
35
+ | "stale_draft"
36
+ | "never_sent"
37
+ | "missing_locale";
@@ -2,3 +2,5 @@ export * from "./blocks";
2
2
  export * from "./resolved";
3
3
  export * from "./sends";
4
4
  export * from "./settings";
5
+ export * from "./suppressions";
6
+ export * from "./webhook";
@@ -1,4 +1,7 @@
1
- import type { SendStatus } from "@antelopejs/interface-dms-mailing";
1
+ import type {
2
+ SendEventType,
3
+ SendStatus,
4
+ } from "@antelopejs/interface-dms-mailing";
2
5
 
3
6
  /**
4
7
  * Send statuses and provider event types are part of the public interface:
@@ -10,7 +13,8 @@ export type {
10
13
  SendStatus,
11
14
  } from "@antelopejs/interface-dms-mailing";
12
15
 
13
- export const SEND_STATUSES: SendStatus[] = [
16
+ /** What a provider reports; a webhook carries one of these. */
17
+ export const SEND_EVENT_TYPES: SendEventType[] = [
14
18
  "queued",
15
19
  "sent",
16
20
  "delivered",
@@ -22,8 +26,62 @@ export const SEND_STATUSES: SendStatus[] = [
22
26
  "unsubscribed",
23
27
  ];
24
28
 
29
+ /** Every status of a send: the provider's events, and `suppressed`. */
30
+ export const SEND_STATUSES: SendStatus[] = [...SEND_EVENT_TYPES, "suppressed"];
31
+
25
32
  export const PROBLEM_STATUSES: SendStatus[] = ["bounced", "failed", "spam"];
26
33
 
34
+ /**
35
+ * The send log tab a status belongs to. Stored next to the status so a tab is a
36
+ * single-value filter: a view filter is AND-only and `is` compares one value.
37
+ * A problem someone already sent again or to another address is `resolved`:
38
+ * it leaves the Problems tab, the nav badge and the provider banner.
39
+ */
40
+ export type SendStage =
41
+ | "problem"
42
+ | "in_progress"
43
+ | "delivered"
44
+ | "engaged"
45
+ | "unsubscribed"
46
+ | "suppressed"
47
+ | "resolved";
48
+
49
+ export const SEND_STAGES: SendStage[] = [
50
+ "problem",
51
+ "in_progress",
52
+ "delivered",
53
+ "engaged",
54
+ "unsubscribed",
55
+ "suppressed",
56
+ "resolved",
57
+ ];
58
+
59
+ export const STAGE_BY_STATUS: Record<SendStatus, SendStage> = {
60
+ queued: "in_progress",
61
+ sent: "in_progress",
62
+ delivered: "delivered",
63
+ opened: "engaged",
64
+ clicked: "engaged",
65
+ bounced: "problem",
66
+ spam: "problem",
67
+ failed: "problem",
68
+ unsubscribed: "unsubscribed",
69
+ suppressed: "suppressed",
70
+ };
71
+
72
+ const RESOLVABLE_STAGE: SendStage = "problem";
73
+ export const RESOLVED_STAGE: SendStage = "resolved";
74
+
75
+ export function stageOf(status: SendStatus, isResolved = false): SendStage {
76
+ const stage = STAGE_BY_STATUS[status];
77
+ return isResolved && stage === RESOLVABLE_STAGE ? RESOLVED_STAGE : stage;
78
+ }
79
+
80
+ /** Whether someone already dealt with this send's problem. */
81
+ export function isResolvedStage(stage: SendStage | undefined): boolean {
82
+ return stage === RESOLVED_STAGE;
83
+ }
84
+
27
85
  /**
28
86
  * Which sends a metric describes. The Overview is the business view and hides
29
87
  * test sends; the Sends page is the operational log and counts what actually
@@ -41,4 +99,8 @@ export interface SendEventDetails {
41
99
  userAgent?: string;
42
100
  reason?: string;
43
101
  code?: string;
102
+ /** The provider's own id of the event, to ignore a webhook it retries. */
103
+ eventId?: string;
104
+ /** `soft`/`transient` or `hard`/`permanent`, when the provider says so. */
105
+ bounceType?: string;
44
106
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Why an address is on the suppression list: the receiving server said it
3
+ * does not exist, the recipient reported spam or unsubscribed, or someone
4
+ * added it by hand.
5
+ */
6
+ export type SuppressionReason =
7
+ | "hard_bounce"
8
+ | "complaint"
9
+ | "unsubscribe"
10
+ | "manual";
11
+
12
+ export const SUPPRESSION_REASONS: SuppressionReason[] = [
13
+ "hard_bounce",
14
+ "complaint",
15
+ "unsubscribe",
16
+ "manual",
17
+ ];
18
+
19
+ /** What the send log and its drawer say about a suppressed address. */
20
+ export interface SuppressionSummary {
21
+ id: string;
22
+ email: string;
23
+ reason: SuppressionReason;
24
+ createdAt: Date;
25
+ /** The send whose event put the address on the list. */
26
+ sendId?: string;
27
+ }
@@ -0,0 +1,29 @@
1
+ /** Why the webhook refused a request, as the settings tell it. */
2
+ export type WebhookRefusal = "missing_secret" | "bad_secret" | "invalid_event";
3
+
4
+ /** The last request the webhook accepted. */
5
+ export interface WebhookReceipt {
6
+ at: Date;
7
+ provider: string;
8
+ type: string;
9
+ /**
10
+ * Whether the event was recorded against a send: not when no send carries
11
+ * its message id, nor when the provider delivered it twice.
12
+ */
13
+ isMatched: boolean;
14
+ /** Sent by the settings' "Send a test event" button. */
15
+ isTest: boolean;
16
+ }
17
+
18
+ /** The last request the webhook refused. */
19
+ export interface WebhookRejection {
20
+ at: Date;
21
+ provider: string;
22
+ reason: WebhookRefusal;
23
+ }
24
+
25
+ /** What the settings show about the webhook. */
26
+ export interface WebhookStatus {
27
+ lastReceived: WebhookReceipt | null;
28
+ lastRefused: WebhookRejection | null;
29
+ }
@@ -1,33 +1,189 @@
1
+ import { domainToASCII } from "node:url";
1
2
  import { z } from "zod";
2
- import { MAX_NAME_LENGTH } from "../constants";
3
+ import { MAX_NAME_LENGTH, SLUG_PATTERN } from "../constants";
4
+ import { MARKETING_CATEGORY } from "../engine";
3
5
 
4
- const MIN_RETENTION_DAYS = 1;
5
- const MAX_RETENTION_DAYS = 3650;
6
- const LOCALE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;
7
- // The generated secret is 48 hex characters; this floor lets an admin paste a
8
- // provider-issued one without letting them blank the field, which would leave
9
- // the unauthenticated event endpoint with no gate at all.
10
- const MIN_WEBHOOK_SECRET_LENGTH = 32;
6
+ export const MIN_RETENTION_DAYS = 1;
7
+ export const MAX_RETENTION_DAYS = 3650;
8
+ const MAX_LOCALE_LENGTH = 35;
9
+ const CATEGORY_ICON_PATTERN = /^i-ph-[a-z0-9-]+$/;
10
+ const CONTROL_CHARACTERS = /\p{Cc}/u;
11
+ const ADDRESS_SEPARATOR = "@";
12
+ const NO_ADDRESS = "";
13
+ /**
14
+ * The generated secret is 48 hex characters; this floor lets an admin paste a
15
+ * provider-issued one without letting them blank the field, which would leave
16
+ * the unauthenticated event endpoint with no gate at all.
17
+ */
18
+ export const MIN_WEBHOOK_SECRET_LENGTH = 32;
19
+
20
+ const messageKey = (path: string): string => `$dms_mailing.settings.${path}`;
21
+ const LABEL_REQUIRED = messageKey("categories.label_required");
22
+ const CATEGORY_DUPLICATE = messageKey("categories.label_duplicate");
23
+ const MARKETING_REQUIRED = messageKey("categories.marketing_required");
24
+ const LABEL_PART = "label";
25
+ const UNKNOWN_MOVE_TARGET = "$dms.field_errors.invalid";
26
+ export const WEBHOOK_SECRET_TOO_SHORT = messageKey(
27
+ "fields.webhookSecret.too_short",
28
+ );
29
+
30
+ const MISSING_VARIABLES_POLICIES = ["refuse", "send"] as const;
11
31
 
12
32
  export const categorySchema = z.object({
13
- id: z.string().min(1).max(MAX_NAME_LENGTH),
14
- label: z.string().min(1).max(MAX_NAME_LENGTH),
15
- icon: z.string().min(1),
33
+ id: z.string().max(MAX_NAME_LENGTH).regex(SLUG_PATTERN),
34
+ label: z.string().trim().max(MAX_NAME_LENGTH),
35
+ icon: z.string().regex(CATEGORY_ICON_PATTERN),
16
36
  });
17
37
 
38
+ /**
39
+ * A category as the settings form sends it: a kept one, or one removed whose
40
+ * templates go to `moveTo` (another kept category) once saved.
41
+ */
42
+ const categoryEntrySchema = categorySchema.extend({
43
+ moveTo: z.string().regex(SLUG_PATTERN).optional(),
44
+ });
45
+
46
+ export type CategoryEntry = z.infer<typeof categoryEntrySchema>;
47
+
48
+ /** Why a category list is refused, and the part of it at fault. */
49
+ interface CategoryIssue {
50
+ message: string;
51
+ path: Array<string | number>;
52
+ }
53
+
54
+ const isKept = (entry: CategoryEntry): boolean => entry.moveTo === undefined;
55
+
56
+ /**
57
+ * A kept entry repeating the label (whatever its case) or the id of an
58
+ * earlier one, under the row it belongs to.
59
+ */
60
+ function duplicateIssues(entries: CategoryEntry[]): CategoryIssue[] {
61
+ const labels = new Set<string>();
62
+ const ids = new Set<string>();
63
+ const issues: CategoryIssue[] = [];
64
+ entries.forEach((entry, index) => {
65
+ if (!isKept(entry) || !entry.label) return;
66
+ const label = entry.label.toLocaleLowerCase();
67
+ if (labels.has(label))
68
+ issues.push({ message: CATEGORY_DUPLICATE, path: [index, LABEL_PART] });
69
+ else if (ids.has(entry.id))
70
+ issues.push({ message: CATEGORY_DUPLICATE, path: [index, "id"] });
71
+ labels.add(label);
72
+ ids.add(entry.id);
73
+ });
74
+ return issues;
75
+ }
76
+
77
+ /**
78
+ * A row left without a label. Checked with the whole list rather than as the
79
+ * label's own minimum, which the form would turn into a browser-side check:
80
+ * the server words it, under the row, together with the other row issues.
81
+ */
82
+ function blankLabelIssues(entries: CategoryEntry[]): CategoryIssue[] {
83
+ return entries.flatMap((entry, index) =>
84
+ entry.label ? [] : [{ message: LABEL_REQUIRED, path: [index, LABEL_PART] }],
85
+ );
86
+ }
87
+
88
+ function marketingIssues(kept: Set<string>): CategoryIssue[] {
89
+ return kept.has(MARKETING_CATEGORY)
90
+ ? []
91
+ : [{ message: MARKETING_REQUIRED, path: [] }];
92
+ }
93
+
94
+ function moveIssues(
95
+ entries: CategoryEntry[],
96
+ kept: Set<string>,
97
+ ): CategoryIssue[] {
98
+ return entries.flatMap((entry, index) =>
99
+ isKept(entry) || kept.has(entry.moveTo as string)
100
+ ? []
101
+ : [{ message: UNKNOWN_MOVE_TARGET, path: [index, "moveTo"] }],
102
+ );
103
+ }
104
+
105
+ function refineCategoryEntries(
106
+ entries: CategoryEntry[],
107
+ context: z.RefinementCtx,
108
+ ): void {
109
+ const kept = new Set(entries.filter(isKept).map((entry) => entry.id));
110
+ [
111
+ ...blankLabelIssues(entries),
112
+ ...duplicateIssues(entries),
113
+ ...marketingIssues(kept),
114
+ ...moveIssues(entries, kept),
115
+ ].forEach((issue) =>
116
+ context.addIssue({ code: z.ZodIssueCode.custom, ...issue }),
117
+ );
118
+ }
119
+
120
+ /**
121
+ * The category list of the settings: kept labels and ids are unique, every
122
+ * move names a kept one, and the marketing category stays.
123
+ */
124
+ export const categoriesSchema = z
125
+ .array(categoryEntrySchema)
126
+ .superRefine(refineCategoryEntries);
127
+
128
+ function isWellFormedLocale(value: string): boolean {
129
+ try {
130
+ return Intl.getCanonicalLocales(value).length === 1;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ /** An address as typed or pasted, its domain in the ASCII form mail uses. */
137
+ function toAsciiAddress(address: string): string {
138
+ const separator = address.lastIndexOf(ADDRESS_SEPARATOR);
139
+ if (separator < 0) return address;
140
+ const domain = domainToASCII(address.slice(separator + 1));
141
+ return domain ? `${address.slice(0, separator + 1)}${domain}` : address;
142
+ }
143
+
144
+ const optionalAddressSchema = z
145
+ .string()
146
+ .trim()
147
+ .transform(toAsciiAddress)
148
+ .pipe(z.union([z.literal(NO_ADDRESS), z.string().email()]));
149
+
18
150
  export const settingsSchema = z.object({
19
- fallbackLocale: z.string().regex(LOCALE_PATTERN),
151
+ fallbackLocale: z
152
+ .string()
153
+ .trim()
154
+ .max(MAX_LOCALE_LENGTH)
155
+ .refine(isWellFormedLocale),
20
156
  logRetentionDays: z
21
157
  .number()
22
158
  .int()
23
159
  .min(MIN_RETENTION_DAYS)
24
160
  .max(MAX_RETENTION_DAYS),
25
161
  blockOnMissingVariables: z.boolean(),
26
- senderName: z.string().max(MAX_NAME_LENGTH).default(""),
27
- senderEmail: z.union([z.string().email(), z.literal("")]).default(""),
28
- replyTo: z.union([z.string().email(), z.literal("")]).default(""),
29
- webhookSecret: z.string().min(MIN_WEBHOOK_SECRET_LENGTH).max(MAX_NAME_LENGTH),
30
- categories: z.array(categorySchema).default([]),
162
+ senderName: z
163
+ .string()
164
+ .trim()
165
+ .max(MAX_NAME_LENGTH)
166
+ .refine((value) => !CONTROL_CHARACTERS.test(value)),
167
+ senderEmail: optionalAddressSchema,
168
+ replyTo: optionalAddressSchema,
169
+ webhookSecret: z
170
+ .string()
171
+ .min(MIN_WEBHOOK_SECRET_LENGTH, WEBHOOK_SECRET_TOO_SHORT)
172
+ .max(MAX_NAME_LENGTH),
173
+ categories: categoriesSchema,
31
174
  });
32
175
 
176
+ /**
177
+ * What a write may carry: any of the settings (`null` already turned into
178
+ * `""` for a cleared one) and the missing-variables choice the form shows.
179
+ * Anything else is refused.
180
+ */
181
+ export const settingsPatchSchema = settingsSchema
182
+ .partial()
183
+ .extend({
184
+ missingVariables: z.enum(MISSING_VARIABLES_POLICIES).optional(),
185
+ })
186
+ .strict();
187
+
188
+ export type SettingsPatch = z.infer<typeof settingsPatchSchema>;
33
189
  export type SettingsInput = z.input<typeof settingsSchema>;
@@ -18,6 +18,8 @@ export const createTemplateSchema = z.object({
18
18
  category: z.string().optional(),
19
19
  /** Absent or empty starts blank; otherwise the template to copy. */
20
20
  sourceTemplateId: z.string().optional(),
21
+ /** A starter template to start from, when no template is copied. */
22
+ starterId: z.string().optional(),
21
23
  });
22
24
 
23
25
  export const conditionSchema = z.object({
@@ -99,6 +101,20 @@ export const templateContentSchema = z.object({
99
101
  locales: z.record(localeContentSchema),
100
102
  });
101
103
 
104
+ /**
105
+ * The draft revision the writer last saw; a write behind the stored draft is
106
+ * refused. Absent writes unconditionally.
107
+ */
108
+ export const draftRevisionSchema = z.number().int().min(0).optional();
109
+
110
+ export const publishSchema = z.object({ revision: draftRevisionSchema });
111
+
112
+ /** Restoring a version writes the draft: it carries the revision too. */
113
+ export const restoreVersionSchema = publishSchema;
114
+
115
+ /** A published version's number, as the route's parameter spells it. */
116
+ export const versionNumberSchema = z.coerce.number().int().min(1);
117
+
102
118
  export const variableSchema = z.object({
103
119
  path: z.string().min(1),
104
120
  type: z.enum([
@@ -114,13 +130,40 @@ export const variableSchema = z.object({
114
130
  required: z.boolean(),
115
131
  });
116
132
 
117
- export const variablesSchema = z.object({ variables: z.array(variableSchema) });
133
+ export const variableListSchema = z.array(variableSchema);
134
+
135
+ /** Declarations land in the draft, like the content: same revision check. */
136
+ export const variablesSchema = z.object({
137
+ variables: variableListSchema,
138
+ revision: draftRevisionSchema,
139
+ });
140
+
141
+ /**
142
+ * A draft save: the content and, when given, the variable declarations
143
+ * (absent keeps the draft's own).
144
+ */
145
+ export const saveContentSchema = z.object({
146
+ content: templateContentSchema,
147
+ variables: variableListSchema.optional(),
148
+ revision: draftRevisionSchema,
149
+ });
118
150
 
119
151
  export const testDataSchema = z.object({ data: z.record(z.unknown()) });
120
152
 
153
+ /**
154
+ * Which content a preview renders when it carries none: the working draft
155
+ * (the default), what customers receive, or a published version number.
156
+ */
157
+ export const previewVersionSchema = z.union([
158
+ z.literal("draft"),
159
+ z.literal("published"),
160
+ z.number().int().min(1),
161
+ ]);
162
+
121
163
  export const previewSchema = z.object({
122
164
  /** Absent lets the server pick the tenant fallback (see `pickLocale`). */
123
165
  locale: z.string().min(1).optional(),
166
+ version: previewVersionSchema.optional(),
124
167
  content: templateContentSchema.optional(),
125
168
  data: z.record(z.unknown()).optional(),
126
169
  });
@@ -143,3 +186,12 @@ export const duplicateSchema = z.object({
143
186
  slug: slugSchema,
144
187
  name: z.string().trim().min(1).max(MAX_NAME_LENGTH),
145
188
  });
189
+
190
+ /** A send again; `to` corrects the address of a bounced send. */
191
+ export const replaySchema = z.object({
192
+ to: z.string().trim().email().optional(),
193
+ });
194
+
195
+ export const replayFailedSchema = z.object({
196
+ since: z.coerce.date().optional(),
197
+ });
@@ -1,32 +0,0 @@
1
- export declare const SHARED_MODULE_SOURCES: {
2
- readonly dms: {
3
- readonly type: "package";
4
- readonly package: "@antelopejs/dms";
5
- readonly version: ">=0.4.3 <1.0.0";
6
- };
7
- readonly mongodb: {
8
- readonly type: "package";
9
- readonly package: "@antelopejs/mongodb";
10
- readonly version: "^1.3.1";
11
- };
12
- readonly "auth-jwt": {
13
- readonly type: "package";
14
- readonly package: "@antelopejs/auth-jwt";
15
- readonly version: "^1.0.3";
16
- };
17
- readonly api: {
18
- readonly type: "package";
19
- readonly package: "@antelopejs/api";
20
- readonly version: "^1.3.0";
21
- };
22
- readonly "file-storage-local": {
23
- readonly type: "package";
24
- readonly package: "@antelopejs/file-storage-local";
25
- readonly version: "^0.1.5";
26
- };
27
- readonly nodemailer: {
28
- readonly type: "package";
29
- readonly package: "@antelopejs/nodemailer";
30
- readonly version: "^0.0.5";
31
- };
32
- };