@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
@@ -10,46 +10,91 @@ import {
10
10
  } from "@antelopejs/interface-api";
11
11
  import { assert, assertValidation } from "@antelopejs/interface-api-util";
12
12
  import { AuthUserWithPermission } from "@antelopejs/interface-dms/guards";
13
+ import { ZodError } from "zod";
13
14
  import { getRequestTenantId } from "@antelopejs/interface-dms/request-tenant";
14
15
  import { TenantScopedModel } from "@antelopejs/interface-dms/tenant-scoped-model";
15
16
  import type { User } from "@antelopejs/interface-dms/auth/db";
17
+ import type { SendTemplateResult } from "@antelopejs/interface-dms-mailing";
18
+ import type {
19
+ BannerContent,
20
+ StatGroupItem,
21
+ } from "@antelopejs/interface-dms/base";
16
22
  import {
17
23
  API_BASE_PATH,
24
+ HTTP_CONFLICT,
18
25
  HTTP_NOT_FOUND,
19
26
  HTTP_UNPROCESSABLE,
20
27
  } from "../constants";
21
28
  import {
22
29
  type MailingSend,
23
30
  type MailingSendEvent,
31
+ type MailingTemplate,
32
+ normalizeAddress,
24
33
  SendEventModel,
34
+ type SendSummary,
25
35
  SendModel,
26
36
  TemplateModel,
27
37
  } from "../db";
28
38
  import { resolveEmail } from "../engine";
29
- import type {
30
- SendTemplateParams,
31
- SendTemplateResult,
32
- } from "@antelopejs/interface-dms-mailing";
39
+ import type { InterpolationMode } from "../engine/tokens";
33
40
  import { SendsPageController } from "../pages/sends";
41
+ import { readCapabilities } from "../services/provider";
42
+ import { buildProviderBanner } from "../services/provider-banner";
34
43
  import { renderEmailHtml } from "../services/render";
35
- import { SendError, sendTemplate } from "../services/send";
44
+ import {
45
+ type CheckedSendParams,
46
+ sendUnlessSuppressed,
47
+ } from "../services/checked-send";
48
+ import { MANUAL_SEND_SOURCE, SendError, subjectFor } from "../services/send";
49
+ import {
50
+ buildSendStats,
51
+ isProviderFailure,
52
+ isTransportFailure,
53
+ MAX_BULK_REPLAY,
54
+ type SendStats,
55
+ trackingOf,
56
+ } from "../services/send-stats";
57
+ import { buildSendStatItems } from "../services/send-stat-items";
58
+ import type { DateRange } from "../services/metrics";
36
59
  import { assertSendPermission } from "../services/permissions";
37
60
  import { getSettings } from "../services/settings";
61
+ import { findSuppression, suppressedSendIds } from "../services/suppressions";
38
62
  import {
63
+ contentOfVersion,
39
64
  localeContentOf,
40
- parseContent,
41
65
  pickLocale,
66
+ publishedVersionOf,
67
+ requireTemplate,
42
68
  } from "../services/templates";
43
- import type { TemplateContent } from "../types";
44
- import { realSendSchema, testSendSchema } from "../validation/templates.schema";
69
+ import type {
70
+ SendStatus,
71
+ SuppressionSummary,
72
+ TemplateContent,
73
+ TemplateStatus,
74
+ } from "../types";
75
+ import {
76
+ realSendSchema,
77
+ replayFailedSchema,
78
+ replaySchema,
79
+ testSendSchema,
80
+ } from "../validation/templates.schema";
81
+ import { comparisonRange, currentRange, isComparisonWanted } from "./period";
45
82
 
46
83
  const NOT_FOUND = "$dms_mailing.errors.send_not_found";
47
- const TEMPLATE_NOT_FOUND = "$dms_mailing.errors.template_not_found";
48
- const TEMPLATE_GONE = "$dms_mailing.errors.template_not_found";
49
84
  const INVALID_CONTENT = "$dms_mailing.errors.invalid_content";
85
+ const INVALID_RECIPIENT = "$dms_mailing.errors.invalid_recipient";
86
+ const REPLAY_PARTIAL = "$dms_mailing.sends.banner.replay_partial";
87
+ const REPLAY_SKIPPED = "$dms_mailing.sends.banner.replay_skipped";
88
+ const RECIPIENT_FIELD = "to";
89
+ const NEEDS_NEW_ADDRESS = "$dms_mailing.errors.replay_needs_new_address";
90
+ const NO_REPLAY_AFTER_SPAM = "$dms_mailing.errors.no_replay_after_spam";
50
91
  const TEST_SEND_SOURCE = "dms-mailing:test-send";
51
92
  const REPLAY_SOURCE = "dms-mailing:replay";
52
- const MANUAL_SEND_SOURCE = "dms-mailing:manual";
93
+ const FIX_ADDRESS_SOURCE = "dms-mailing:fix-address";
94
+ const HOUR_MS = 3_600_000;
95
+ const DRAFT_VERSION = 0;
96
+ /** Renders as the recipient got it, not as the editor previews it. */
97
+ const SEND_MODE: InterpolationMode = "send";
53
98
 
54
99
  interface TestSendInput {
55
100
  locale: string;
@@ -57,9 +102,130 @@ interface TestSendInput {
57
102
  data?: Record<string, unknown>;
58
103
  }
59
104
 
105
+ export interface SendTemplateSummary {
106
+ id: string;
107
+ name: string;
108
+ status: TemplateStatus;
109
+ /** Version customers receive today, to compare with the send's. */
110
+ publishedVersion: number;
111
+ }
112
+
60
113
  export interface SendDetailResponse {
61
114
  send: MailingSend;
62
115
  events: MailingSendEvent[];
116
+ /** `null` when the template was deleted since. */
117
+ template: SendTemplateSummary | null;
118
+ /** The recipient's entry on the suppression list, `null` when it has none. */
119
+ suppression: SuppressionSummary | null;
120
+ }
121
+
122
+ /**
123
+ * What a test send did. A test to the editor's own address goes out even when
124
+ * the address is suppressed; `ownAddressSuppressed` says so.
125
+ */
126
+ export interface TestSendResponse {
127
+ results: SendTemplateResult[];
128
+ ownAddressSuppressed?: SuppressionSummary;
129
+ }
130
+
131
+ export interface SendStatsResponse extends SendStats {
132
+ items: StatGroupItem[];
133
+ }
134
+
135
+ /**
136
+ * What a send again did. When the address is on the suppression list nothing
137
+ * left: `status` is `suppressed`, the original stays a problem, and `warning`
138
+ * carries the reason for the toast.
139
+ */
140
+ export interface ReplayResponse extends SendTemplateResult {
141
+ warning?: string;
142
+ }
143
+
144
+ /**
145
+ * What "send the failures again" did: sent, failed to send again (could not
146
+ * start, or the address went on the suppression list meanwhile), and left out
147
+ * since the address is on the suppression list.
148
+ */
149
+ export interface ReplayFailedResponse {
150
+ count: number;
151
+ failed: number;
152
+ skipped: number;
153
+ results: SendTemplateResult[];
154
+ /** Tells the toast some failures could not be sent again. */
155
+ warning?: string;
156
+ }
157
+
158
+ export interface SendHtmlResponse {
159
+ html: string;
160
+ subject: string;
161
+ locale: string;
162
+ version: number;
163
+ /**
164
+ * A draft test is rendered from today's draft, which may differ from what
165
+ * was sent.
166
+ */
167
+ approximate: boolean;
168
+ /** The sender of the settings today, not necessarily the one it went with. */
169
+ from: string;
170
+ fromName: string;
171
+ }
172
+
173
+ /** A failure someone else already sent again. */
174
+ const CLAIMED_ELSEWHERE = "claimed-elsewhere";
175
+ /** A failure whose send again could not start: it stays a problem. */
176
+ const NOT_STARTED = "not-started";
177
+
178
+ type ClaimedReplay =
179
+ | SendTemplateResult
180
+ | typeof CLAIMED_ELSEWHERE
181
+ | typeof NOT_STARTED;
182
+
183
+ /** Problem statuses whose address is at fault: repeating the send cannot help. */
184
+ const ADDRESS_PROBLEMS: SendStatus[] = ["bounced"];
185
+
186
+ /** A permanent bounce only takes another address; a temporary one may retry. */
187
+ function needsNewAddress(send: MailingSend): boolean {
188
+ return ADDRESS_PROBLEMS.includes(send.status) && !send.isSoftBounce;
189
+ }
190
+
191
+ const sameAddress = (left: string, right: string): boolean =>
192
+ normalizeAddress(left) === normalizeAddress(right);
193
+
194
+ /** A bad address gets its own message; any other invalid field is the content's. */
195
+ function sendInputError(error: unknown): string {
196
+ const isRecipient =
197
+ error instanceof ZodError &&
198
+ error.issues.some((issue) => issue.path[0] === RECIPIENT_FIELD);
199
+ return isRecipient ? INVALID_RECIPIENT : INVALID_CONTENT;
200
+ }
201
+
202
+ const isSuppressedResult = (result: SendTemplateResult): boolean =>
203
+ result.status === "suppressed";
204
+
205
+ /** A suppressed send again carries its reason as a warning. */
206
+ function replayResponse(result: SendTemplateResult): ReplayResponse {
207
+ return isSuppressedResult(result)
208
+ ? { ...result, warning: result.error }
209
+ : result;
210
+ }
211
+
212
+ function replayReport(
213
+ results: SendTemplateResult[],
214
+ notStarted: number,
215
+ skipped: number,
216
+ ): ReplayFailedResponse {
217
+ const suppressed = results.filter(isSuppressedResult).length;
218
+ const report: ReplayFailedResponse = {
219
+ count: results.length - suppressed,
220
+ failed: notStarted + suppressed,
221
+ skipped,
222
+ results,
223
+ };
224
+ const hasFailures =
225
+ report.failed > 0 || results.some((result) => result.status === "failed");
226
+ if (hasFailures) report.warning = REPLAY_PARTIAL;
227
+ else if (skipped > 0) report.warning = REPLAY_SKIPPED;
228
+ return report;
63
229
  }
64
230
 
65
231
  export class SendsController extends Controller(API_BASE_PATH) {
@@ -92,28 +258,42 @@ export class SendsController extends Controller(API_BASE_PATH) {
92
258
  async testSend(
93
259
  @Parameter("id", "param") id: string,
94
260
  @JSONBody() body: unknown,
95
- ) {
261
+ ): Promise<TestSendResponse> {
96
262
  await assertSendPermission(this.user, this.tenantId);
97
263
  const input = assertValidation(
98
264
  body,
99
265
  (value) => testSendSchema.parse(value),
100
- () => INVALID_CONTENT,
266
+ sendInputError,
101
267
  );
102
- const template = await this.templates.get(id);
103
- assert(template, HTTP_NOT_FOUND, TEMPLATE_NOT_FOUND);
268
+ const template = await requireTemplate(this.templates, id);
269
+ const ownAddressSuppressed = await this.ownSuppression(input.to);
104
270
  const result = await this.send(
105
271
  {
106
272
  tenantId: this.tenantId,
107
273
  to: input.to,
274
+ allowSuppressed: [this.user.email],
108
275
  locale: (input as TestSendInput).locale,
109
276
  variables: (input as TestSendInput).data,
110
277
  content: (input as TestSendInput).content,
278
+ version: requestedVersionOf(input.version),
111
279
  isTest: true,
112
280
  source: TEST_SEND_SOURCE,
113
281
  },
114
282
  template.slug,
115
283
  );
116
- return { results: result.results ?? [result] };
284
+ return {
285
+ results: result.results ?? [result],
286
+ ...(ownAddressSuppressed && { ownAddressSuppressed }),
287
+ };
288
+ }
289
+
290
+ /** The editor's own entry on the suppression list, when a test goes to it. */
291
+ private async ownSuppression(
292
+ to: string[],
293
+ ): Promise<SuppressionSummary | undefined> {
294
+ const isOwn = (address: string) => sameAddress(address, this.user.email);
295
+ if (!to.some(isOwn)) return undefined;
296
+ return (await findSuppression(this.tenantId, this.user.email)) ?? undefined;
117
297
  }
118
298
 
119
299
  /**
@@ -129,10 +309,9 @@ export class SendsController extends Controller(API_BASE_PATH) {
129
309
  const input = assertValidation(
130
310
  body,
131
311
  (value) => realSendSchema.parse(value),
132
- () => INVALID_CONTENT,
312
+ sendInputError,
133
313
  );
134
- const template = await this.templates.get(id);
135
- assert(template, HTTP_NOT_FOUND, TEMPLATE_NOT_FOUND);
314
+ const template = await requireTemplate(this.templates, id);
136
315
  const result = await this.send(
137
316
  {
138
317
  tenantId: this.tenantId,
@@ -147,6 +326,49 @@ export class SendsController extends Controller(API_BASE_PATH) {
147
326
  return { results: result.results ?? [result] };
148
327
  }
149
328
 
329
+ /**
330
+ * The stat strip of the send log, for the period the page selected: the
331
+ * figures, and the `StatGroup` items that word them.
332
+ */
333
+ @Get("/sends/stats")
334
+ async stats(): Promise<SendStatsResponse> {
335
+ const range = currentRange(this.ctx);
336
+ const [current, previous, capabilities] = await Promise.all([
337
+ this.sends.listBetween(range.from, range.to),
338
+ this.previousSends(range),
339
+ readCapabilities(),
340
+ ]);
341
+ const stats = buildSendStats(current, previous, trackingOf(capabilities));
342
+ return {
343
+ ...stats,
344
+ items: buildSendStatItems(stats, capabilities?.name ?? ""),
345
+ };
346
+ }
347
+
348
+ /**
349
+ * The provider banner of the send log: what is wrong with the provider and
350
+ * the failures of the last hour a send again may fix, or nothing.
351
+ */
352
+ @Get("/sends/banner")
353
+ async banner(): Promise<BannerContent | null> {
354
+ const since = new Date(Date.now() - HOUR_MS);
355
+ const [capabilities, recent] = await Promise.all([
356
+ readCapabilities(),
357
+ this.sends.listBetween(since, new Date()),
358
+ ]);
359
+ const failures = recent.filter(isProviderFailure);
360
+ const suppressed = await suppressedSendIds(this.tenantId, failures);
361
+ return buildProviderBanner({
362
+ providerName: capabilities?.name ?? "",
363
+ providerReachable: capabilities !== null,
364
+ recentFailures: failures.length,
365
+ transportFailures: recent.filter(isTransportFailure).length,
366
+ suppressedFailures: suppressed.size,
367
+ recentSends: recent.filter((send) => !send.isTest).length,
368
+ since,
369
+ });
370
+ }
371
+
150
372
  @Get("/sends/:id")
151
373
  async detail(
152
374
  @Parameter("id", "param") id: string,
@@ -154,54 +376,183 @@ export class SendsController extends Controller(API_BASE_PATH) {
154
376
  const send = await this.requireSend(id);
155
377
  const events = await this.events.getBySend(id);
156
378
  events.sort((left, right) => left.at.getTime() - right.at.getTime());
157
- return { send, events };
379
+ const [template, suppression] = await Promise.all([
380
+ this.templates.get(send.templateId),
381
+ findSuppression(this.tenantId, send.recipientEmail),
382
+ ]);
383
+ return {
384
+ send,
385
+ events,
386
+ template: template ? summarize(template) : null,
387
+ suppression,
388
+ };
158
389
  }
159
390
 
391
+ /**
392
+ * What the recipient received: the version the send used, rendered with the
393
+ * variables it carried.
394
+ */
160
395
  @Get("/sends/:id/html")
161
- async html(@Parameter("id", "param") id: string) {
396
+ async html(@Parameter("id", "param") id: string): Promise<SendHtmlResponse> {
162
397
  const send = await this.requireSend(id);
163
- // There is no version history: this re-renders the template as it stands
164
- // today, which may differ from what actually went out.
165
- const template = await this.templates.get(send.templateId);
166
- assert(template, HTTP_NOT_FOUND, TEMPLATE_GONE);
167
- const content = parseContent(template);
398
+ const template = await requireTemplate(this.templates, send.templateId);
399
+ const content = await contentOfVersion(
400
+ this.tenantId,
401
+ template,
402
+ send.templateVersion,
403
+ );
168
404
  const settings = await getSettings(this.tenantId);
169
405
  const locale = pickLocale(content, send.locale, settings.fallbackLocale);
170
406
  const localeContent = localeContentOf(content, locale);
171
407
  assert(localeContent, HTTP_UNPROCESSABLE, INVALID_CONTENT);
172
- const email = resolveEmail(
173
- localeContent,
174
- JSON.parse(send.json_variables || "{}") as Record<string, unknown>,
175
- );
176
- return { html: await renderEmailHtml(email, locale) };
408
+ const email = resolveEmail(localeContent, variablesOf(send), SEND_MODE);
409
+ const version = send.templateVersion;
410
+ return {
411
+ html: await renderEmailHtml(email, locale),
412
+ subject: subjectFor(email.subject, send.isTest),
413
+ locale,
414
+ version,
415
+ approximate: version === DRAFT_VERSION,
416
+ from: settings.senderEmail,
417
+ fromName: settings.senderName,
418
+ };
177
419
  }
178
420
 
421
+ /**
422
+ * Sends the same version and data again, and marks the original as dealt
423
+ * with. A permanent bounce only takes another address; a spam report is
424
+ * never answered with another e-mail.
425
+ */
179
426
  @Post("/sends/:id/replay")
180
- async replay(@Parameter("id", "param") id: string) {
427
+ async replay(
428
+ @Parameter("id", "param") id: string,
429
+ @JSONBody() body: unknown,
430
+ ): Promise<ReplayResponse> {
181
431
  await assertSendPermission(this.user, this.tenantId);
432
+ const { to } = assertValidation(body ?? {}, (value) =>
433
+ replaySchema.parse(value),
434
+ );
182
435
  const send = await this.requireSend(id);
436
+ assert(send.status !== "spam", HTTP_CONFLICT, NO_REPLAY_AFTER_SPAM);
437
+ const isFix = needsNewAddress(send);
438
+ const isNewAddress = !!to && !sameAddress(to, send.recipientEmail);
439
+ assert(!isFix || isNewAddress, HTTP_CONFLICT, NEEDS_NEW_ADDRESS);
440
+ const result = await this.repeat(
441
+ send,
442
+ to,
443
+ isFix ? FIX_ADDRESS_SOURCE : REPLAY_SOURCE,
444
+ );
445
+ await this.recordReplay(send._id, result);
446
+ return replayResponse(result);
447
+ }
448
+
449
+ /**
450
+ * Sends again every provider failure since `since` (default: the last
451
+ * hour), the first {@link MAX_BULK_REPLAY}. Each one is claimed before it
452
+ * goes, so running it twice never sends a failure twice. A failure whose
453
+ * address is on the suppression list is left out: it stays a problem.
454
+ */
455
+ @Post("/sends/replay-failed")
456
+ async replayFailed(@JSONBody() body: unknown): Promise<ReplayFailedResponse> {
457
+ await assertSendPermission(this.user, this.tenantId);
458
+ const { since } = assertValidation(body ?? {}, (value) =>
459
+ replayFailedSchema.parse(value),
460
+ );
461
+ const from = since ?? new Date(Date.now() - HOUR_MS);
462
+ const rows = await this.sends.listBetween(from, new Date());
463
+ const failures = rows.filter(isProviderFailure);
464
+ const suppressed = await suppressedSendIds(this.tenantId, failures);
465
+ const sendable = failures
466
+ .filter((failure) => !suppressed.has(failure._id))
467
+ .slice(0, MAX_BULK_REPLAY);
468
+ const results: SendTemplateResult[] = [];
469
+ let failed = 0;
470
+ for (const failure of sendable) {
471
+ const outcome = await this.replayClaimed(failure._id);
472
+ if (outcome === CLAIMED_ELSEWHERE) continue;
473
+ if (outcome === NOT_STARTED) failed++;
474
+ else results.push(outcome);
475
+ }
476
+ return replayReport(results, failed, suppressed.size);
477
+ }
478
+
479
+ /** Sends one failure again unless someone already did. */
480
+ private async replayClaimed(sendId: string): Promise<ClaimedReplay> {
481
+ if (!(await this.sends.claimResolution(sendId, this.user._id)))
482
+ return CLAIMED_ELSEWHERE;
483
+ try {
484
+ const send = await this.requireSend(sendId);
485
+ const result = await this.repeat(send, undefined, REPLAY_SOURCE);
486
+ await this.recordReplay(sendId, result);
487
+ if (isSuppressedResult(result))
488
+ await this.sends.releaseResolution(sendId);
489
+ return result;
490
+ } catch {
491
+ await this.sends.releaseResolution(sendId);
492
+ return NOT_STARTED;
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Links the original and the send that repeated it, both ways. A send again
498
+ * the suppression list stopped only points back: nothing left, so the
499
+ * original stays a problem.
500
+ */
501
+ private async recordReplay(
502
+ originalId: string,
503
+ result: SendTemplateResult,
504
+ ): Promise<void> {
505
+ if (!result.sendId) return;
506
+ if (isSuppressedResult(result)) {
507
+ await this.sends.linkReplayOf(result.sendId, originalId);
508
+ return;
509
+ }
510
+ await this.sends.markReplayed(originalId, {
511
+ replayedBy: result.sendId,
512
+ resolvedBy: this.user._id,
513
+ });
514
+ await this.sends.linkReplayOf(result.sendId, originalId);
515
+ }
516
+
517
+ private async repeat(
518
+ send: MailingSend,
519
+ to: string | undefined,
520
+ source: string,
521
+ ): Promise<SendTemplateResult> {
522
+ const template = await requireTemplate(this.templates, send.templateId);
523
+ const version = send.templateVersion;
524
+ const content =
525
+ version > DRAFT_VERSION
526
+ ? await contentOfVersion(this.tenantId, template, version)
527
+ : undefined;
183
528
  return this.send(
184
529
  {
185
530
  tenantId: this.tenantId,
186
- to: send.recipientEmail,
187
- locale: send.locale,
188
- variables: JSON.parse(send.json_variables || "{}") as Record<
189
- string,
190
- unknown
191
- >,
531
+ to: to ?? send.recipientEmail,
532
+ locale: send.requestedLocale || send.locale,
533
+ variables: variablesOf(send),
534
+ content,
535
+ version: content ? version : undefined,
192
536
  isTest: send.isTest,
193
- source: REPLAY_SOURCE,
537
+ source,
194
538
  },
195
539
  send.templateSlug,
196
540
  );
197
541
  }
198
542
 
543
+ /** The sends the strip compares with; `null` when the user wants none. */
544
+ private previousSends(range: DateRange): Promise<SendSummary[] | null> {
545
+ if (!isComparisonWanted(this.ctx)) return Promise.resolve(null);
546
+ const previous = comparisonRange(this.ctx, range);
547
+ return this.sends.listBetween(previous.from, previous.to);
548
+ }
549
+
199
550
  private async send(
200
- params: SendTemplateParams,
551
+ params: CheckedSendParams,
201
552
  slug: string,
202
553
  ): Promise<SendTemplateResult> {
203
554
  try {
204
- return await sendTemplate(slug, params);
555
+ return await sendUnlessSuppressed(slug, params);
205
556
  } catch (error) {
206
557
  if (error instanceof SendError) {
207
558
  throw new HTTPResult(
@@ -213,3 +564,24 @@ export class SendsController extends Controller(API_BASE_PATH) {
213
564
  }
214
565
  }
215
566
  }
567
+
568
+ /**
569
+ * The version a test of a published version records: a number names one,
570
+ * "draft" or "published" let the send decide.
571
+ */
572
+ function requestedVersionOf(version: unknown): number | undefined {
573
+ return typeof version === "number" ? version : undefined;
574
+ }
575
+
576
+ function variablesOf(send: MailingSend): Record<string, unknown> {
577
+ return JSON.parse(send.json_variables || "{}") as Record<string, unknown>;
578
+ }
579
+
580
+ function summarize(template: MailingTemplate): SendTemplateSummary {
581
+ return {
582
+ id: template._id,
583
+ name: template.name,
584
+ status: template.status,
585
+ publishedVersion: publishedVersionOf(template),
586
+ };
587
+ }