@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,181 @@
1
+ import type { SendEventRow, SendRow, SendStatus } from '../types/mailing'
2
+
3
+ export type ProblemKind =
4
+ | 'address_missing'
5
+ | 'mailbox_full'
6
+ | 'rejected_spam'
7
+ | 'spam_report'
8
+ | 'provider_timeout'
9
+ | 'provider_unreachable'
10
+ | 'rejected'
11
+ | 'provider_error'
12
+ | 'unsubscribed'
13
+
14
+ export interface ProblemDescription {
15
+ kind: ProblemKind
16
+ icon: string
17
+ /** i18n key of the plain-language title ("This address doesn't exist"). */
18
+ titleKey: string
19
+ /** i18n key of the one-line explanation under it. */
20
+ descriptionKey: string
21
+ params: Record<string, string>
22
+ }
23
+
24
+ export type ProblemSend = Pick<
25
+ SendRow,
26
+ 'status' | 'error' | 'recipientEmail' | 'provider'
27
+ >
28
+
29
+ interface ProblemRule {
30
+ kind: ProblemKind
31
+ icon: string
32
+ statuses: SendStatus[]
33
+ /** Matched against the raw error; absent matches any error. */
34
+ pattern?: RegExp
35
+ }
36
+
37
+ const KEY_PREFIX = 'dms_mailing.sends.problems.'
38
+ const DOMAIN_SEPARATOR = '@'
39
+
40
+ /**
41
+ * Plain-language readings of a send problem, most specific first: the SMTP
42
+ * class or the provider's wording decides, the status is the fallback.
43
+ */
44
+ const PROBLEM_RULES: ProblemRule[] = [
45
+ {
46
+ kind: 'address_missing',
47
+ icon: 'i-ph-at',
48
+ statuses: ['bounced', 'failed'],
49
+ pattern:
50
+ /5\.1\.[0-3]|\b550\b|\b553\b|does ?n[o']t exist|unknown (user|recipient)|no such (user|mailbox)|user unknown|invalid (recipient|address)/i,
51
+ },
52
+ {
53
+ kind: 'mailbox_full',
54
+ icon: 'i-ph-tray',
55
+ statuses: ['bounced', 'failed'],
56
+ pattern:
57
+ /\b452\b|\b552\b|[45]\.2\.2|mailbox (is )?full|over quota|quota exceeded/i,
58
+ },
59
+ {
60
+ kind: 'rejected_spam',
61
+ icon: 'i-ph-shield-warning',
62
+ statuses: ['bounced', 'failed'],
63
+ pattern: /5\.7\.[0-9]|\bspam\b|blocked|blacklist|reputation/i,
64
+ },
65
+ { kind: 'spam_report', icon: 'i-ph-prohibit', statuses: ['spam'] },
66
+ {
67
+ kind: 'provider_timeout',
68
+ icon: 'i-ph-plugs',
69
+ statuses: ['failed'],
70
+ pattern: /time(d)? ?out|ETIMEDOUT|did ?n[o']t answer/i,
71
+ },
72
+ {
73
+ kind: 'provider_unreachable',
74
+ icon: 'i-ph-plugs',
75
+ statuses: ['failed'],
76
+ pattern:
77
+ /ECONNREFUSED|ECONNRESET|ENOTFOUND|EHOSTUNREACH|ENETUNREACH|EAI_AGAIN|connection refused|unreachable/i,
78
+ },
79
+ { kind: 'rejected', icon: 'i-ph-arrow-u-up-left', statuses: ['bounced'] },
80
+ { kind: 'provider_error', icon: 'i-ph-warning', statuses: ['failed'] },
81
+ { kind: 'unsubscribed', icon: 'i-ph-user-minus', statuses: ['unsubscribed'] },
82
+ ]
83
+
84
+ /** Problems the recipient chose rather than suffered: told in amber. */
85
+ const WARNING_KINDS = new Set<ProblemKind>(['spam_report', 'unsubscribed'])
86
+
87
+ /** The banner tone a problem is told in. */
88
+ export function problemTone(kind: ProblemKind): 'warning' | 'error' {
89
+ return WARNING_KINDS.has(kind) ? 'warning' : 'error'
90
+ }
91
+
92
+ /**
93
+ * Problems whose explanation quotes the provider's words; without them, a
94
+ * sentence that stops short of the colon.
95
+ */
96
+ const KINDS_QUOTING_ERROR = new Set<ProblemKind>(['rejected', 'provider_error'])
97
+ const NO_REASON_SUFFIX = '_no_reason'
98
+ /**
99
+ * Problems whose explanation ends on the advice to send again; when sending
100
+ * again is not possible (the address is on the suppression list), a sentence
101
+ * without that advice.
102
+ */
103
+ const KINDS_ADVISING_SEND_AGAIN = new Set<ProblemKind>([
104
+ 'provider_timeout',
105
+ 'provider_unreachable',
106
+ 'provider_error',
107
+ ])
108
+ const NO_ADVICE_SUFFIX = '_no_advice'
109
+
110
+ function descriptionKey(
111
+ kind: ProblemKind,
112
+ error: string | undefined,
113
+ canSendAgain: boolean,
114
+ ): string {
115
+ let key = `${KEY_PREFIX}${kind}.description`
116
+ if (KINDS_QUOTING_ERROR.has(kind) && !error?.trim()) key += NO_REASON_SUFFIX
117
+ if (!canSendAgain && KINDS_ADVISING_SEND_AGAIN.has(kind))
118
+ key += NO_ADVICE_SUFFIX
119
+ return key
120
+ }
121
+
122
+ function matches(rule: ProblemRule, send: ProblemSend): boolean {
123
+ if (!rule.statuses.includes(send.status)) return false
124
+ return !rule.pattern || rule.pattern.test(send.error ?? '')
125
+ }
126
+
127
+ function domainOf(email: string): string {
128
+ return email.split(DOMAIN_SEPARATOR).at(-1) ?? email
129
+ }
130
+
131
+ /**
132
+ * What went wrong with a send, in the words of the person reading the log:
133
+ * the title and explanation keys plus the icon of the banner. `null` when the
134
+ * send has no problem. `provider` names the provider when the send does not;
135
+ * `canSendAgain` is false when the address can no longer be sent to.
136
+ */
137
+ export function describeProblem(
138
+ send: ProblemSend,
139
+ provider = '',
140
+ canSendAgain = true,
141
+ ): ProblemDescription | null {
142
+ const rule = PROBLEM_RULES.find((candidate) => matches(candidate, send))
143
+ if (!rule) return null
144
+ return {
145
+ kind: rule.kind,
146
+ icon: rule.icon,
147
+ titleKey: `${KEY_PREFIX}${rule.kind}.title`,
148
+ descriptionKey: descriptionKey(rule.kind, send.error, canSendAgain),
149
+ params: {
150
+ provider: send.provider || provider,
151
+ domain: domainOf(send.recipientEmail),
152
+ error: send.error ?? '',
153
+ },
154
+ }
155
+ }
156
+
157
+ const PROBLEM_EVENT_TYPES: SendStatus[] = ['bounced', 'failed', 'spam']
158
+
159
+ interface EventDetails {
160
+ reason?: unknown
161
+ }
162
+
163
+ function reasonOf(event: SendEventRow): string {
164
+ try {
165
+ const details = JSON.parse(event.json_details || '{}') as EventDetails
166
+ return typeof details.reason === 'string' ? details.reason : ''
167
+ } catch {
168
+ return ''
169
+ }
170
+ }
171
+
172
+ /**
173
+ * The provider's words for the latest problem event of a send, for sends
174
+ * logged before their error was kept on the send itself.
175
+ */
176
+ export function problemEventReason(events: SendEventRow[]): string {
177
+ const problems = events.filter((event) =>
178
+ PROBLEM_EVENT_TYPES.includes(event.type),
179
+ )
180
+ return problems.map(reasonOf).filter(Boolean).at(-1) ?? ''
181
+ }
@@ -0,0 +1,49 @@
1
+ import { languageLabel } from './locales'
2
+
3
+ /** A manual send reaches at most this many addresses; a list needs an automation. */
4
+ export const MAX_RECIPIENTS = 5
5
+
6
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
7
+
8
+ export function isEmailAddress(value: string): boolean {
9
+ return EMAIL_PATTERN.test(value.trim())
10
+ }
11
+
12
+ /** Whether a recipient list can be sent: not empty, not over the cap, all addresses. */
13
+ export function isSendableRecipients(recipients: string[]): boolean {
14
+ return (
15
+ recipients.length > 0 &&
16
+ recipients.length <= MAX_RECIPIENTS &&
17
+ recipients.every(isEmailAddress)
18
+ )
19
+ }
20
+
21
+ /** One locale of a select, labelled with its workspace name when known. */
22
+ export interface LocaleOption {
23
+ label: string
24
+ value: string
25
+ }
26
+
27
+ export interface NamedLocale {
28
+ code: string
29
+ name?: string
30
+ }
31
+
32
+ /**
33
+ * Select items for `codes`, named in the interface's language when `uiLocale`
34
+ * is given ("Anglais (EN)" in French), else after the workspace locales.
35
+ */
36
+ export function localeOptions(
37
+ codes: string[],
38
+ workspace: NamedLocale[],
39
+ uiLocale?: string,
40
+ ): LocaleOption[] {
41
+ const names = new Map(workspace.map((entry) => [entry.code, entry.name]))
42
+ return codes.map((code) => {
43
+ const name = names.get(code)
44
+ if (uiLocale)
45
+ return { label: languageLabel(code, uiLocale, name), value: code }
46
+ const upper = code.toUpperCase()
47
+ return { label: name ? `${name} (${upper})` : upper, value: code }
48
+ })
49
+ }
@@ -0,0 +1,23 @@
1
+ import type { SendStatus } from '../types/mailing'
2
+
3
+ const KEY_PREFIX = 'dms_mailing.rendering'
4
+
5
+ /** Statuses that prove the e-mail reached the recipient's mailbox. */
6
+ const RECEIVED_STATUSES: ReadonlySet<SendStatus> = new Set<SendStatus>([
7
+ 'delivered',
8
+ 'opened',
9
+ 'clicked',
10
+ 'spam',
11
+ 'unsubscribed',
12
+ ])
13
+
14
+ /**
15
+ * The i18n key titling a send's rendering: "What {name} received" once the
16
+ * e-mail is known to have arrived, "The e-mail meant for {name}" while it is
17
+ * on its way, bounced, failed or was never sent.
18
+ */
19
+ export function renderingTitleKey(status: SendStatus): string {
20
+ return RECEIVED_STATUSES.has(status)
21
+ ? `${KEY_PREFIX}.received_by`
22
+ : `${KEY_PREFIX}.meant_for`
23
+ }
@@ -0,0 +1,207 @@
1
+ import type { SendRow, SendStatus } from '../types/mailing'
2
+ import { isSuppressedStatus } from './suppression'
3
+
4
+ export type SendActionId =
5
+ 'rendering' | 'copy_address' | 'fix_address' | 'send_again'
6
+
7
+ export type SendActionVariant = 'ghost' | 'outline' | 'solid'
8
+
9
+ export interface SendAction {
10
+ id: SendActionId
11
+ variant: SendActionVariant
12
+ /** i18n key of the button label; absent for an icon-only button. */
13
+ labelKey?: string
14
+ icon: string
15
+ }
16
+
17
+ /**
18
+ * A send as the drawer reads it: what the API added to a send since `SendRow`
19
+ * — a temporary bounce, and the link between a send and its send again.
20
+ */
21
+ export interface DrawerSend extends SendRow {
22
+ isSoftBounce?: boolean
23
+ /** The send this one repeats. */
24
+ replayOf?: string
25
+ /** The send that repeated this one, once someone sent it again. */
26
+ replayedBy?: string
27
+ /** When someone sent it again. */
28
+ resolvedAt?: string
29
+ }
30
+
31
+ /** What decides a drawer's footer. */
32
+ export interface FooterSend extends Pick<
33
+ DrawerSend,
34
+ 'status' | 'isSoftBounce' | 'replayedBy'
35
+ > {
36
+ /**
37
+ * Whether the recipient is on the suppression list today; unknown until
38
+ * the send's detail is loaded.
39
+ */
40
+ isAddressSuppressed?: boolean
41
+ }
42
+
43
+ interface FooterRule {
44
+ matches: (send: FooterSend) => boolean
45
+ actions: SendAction[]
46
+ }
47
+
48
+ const KEY_PREFIX = 'dms_mailing.sends.drawer.actions.'
49
+ const SHORT_ID_LENGTH = 8
50
+
51
+ const RENDERING: SendAction = {
52
+ id: 'rendering',
53
+ variant: 'ghost',
54
+ labelKey: `${KEY_PREFIX}rendering`,
55
+ icon: 'i-ph-eye',
56
+ }
57
+
58
+ const COPY_ADDRESS: SendAction = {
59
+ id: 'copy_address',
60
+ variant: 'outline',
61
+ icon: 'i-ph-copy',
62
+ }
63
+
64
+ const FIX_ADDRESS: SendAction = {
65
+ id: 'fix_address',
66
+ variant: 'solid',
67
+ labelKey: `${KEY_PREFIX}fix_address`,
68
+ icon: 'i-ph-user-circle',
69
+ }
70
+
71
+ const sendAgain = (variant: SendActionVariant): SendAction => ({
72
+ id: 'send_again',
73
+ variant,
74
+ labelKey: `${KEY_PREFIX}send_again`,
75
+ icon: 'i-ph-arrow-clockwise',
76
+ })
77
+
78
+ /** The rendering, one label whatever the status, outlined beside a quiet resend. */
79
+ const RENDERING_OUTLINED: SendAction = { ...RENDERING, variant: 'outline' }
80
+
81
+ /** Footer of a send that went through: repeating it is possible but quiet. */
82
+ const DEFAULT_ACTIONS: SendAction[] = [sendAgain('ghost'), RENDERING_OUTLINED]
83
+
84
+ /** Nothing to send any more: what was received, and the address. */
85
+ const LOOK_ONLY: SendAction[] = [RENDERING, COPY_ADDRESS]
86
+
87
+ /**
88
+ * The drawer footer by status (ML-03): a bounce asks for another address
89
+ * since sending it again would bounce again, a spam report or an
90
+ * unsubscription is never answered with another e-mail, a provider failure is
91
+ * sent again as the main action.
92
+ */
93
+ const ACTIONS_BY_STATUS: Partial<Record<SendStatus, SendAction[]>> = {
94
+ bounced: [RENDERING, COPY_ADDRESS, FIX_ADDRESS],
95
+ spam: LOOK_ONLY,
96
+ unsubscribed: LOOK_ONLY,
97
+ failed: [RENDERING, sendAgain('solid')],
98
+ suppressed: LOOK_ONLY,
99
+ }
100
+
101
+ /**
102
+ * Footers that win over the status: a send someone already sent again offers
103
+ * no second one, a temporary bounce (a full mailbox) may go to the same
104
+ * address later, and so may a suppressed send once its address is
105
+ * reactivated.
106
+ */
107
+ const FOOTER_RULES: FooterRule[] = [
108
+ { matches: (send) => !!send.replayedBy, actions: LOOK_ONLY },
109
+ {
110
+ matches: (send) => send.status === 'bounced' && !!send.isSoftBounce,
111
+ actions: [RENDERING, sendAgain('solid')],
112
+ },
113
+ {
114
+ matches: (send) =>
115
+ isSuppressedStatus(send.status) && send.isAddressSuppressed === false,
116
+ actions: [RENDERING, sendAgain('solid')],
117
+ },
118
+ ]
119
+
120
+ const offersSendAgain = (actions: SendAction[]): boolean =>
121
+ actions.some((action) => action.id === 'send_again')
122
+
123
+ /**
124
+ * The footer buttons of a send's drawer, in display order. While the address
125
+ * is on the suppression list a send again would not leave: the footer only
126
+ * looks, and the drawer's notice links to the list.
127
+ */
128
+ export function sendFooterActions(send: FooterSend): SendAction[] {
129
+ const rule = FOOTER_RULES.find((candidate) => candidate.matches(send))
130
+ const actions =
131
+ rule?.actions ?? ACTIONS_BY_STATUS[send.status] ?? DEFAULT_ACTIONS
132
+ return send.isAddressSuppressed && offersSendAgain(actions)
133
+ ? LOOK_ONLY
134
+ : actions
135
+ }
136
+
137
+ /** The short id the drawer's eyebrow shows ("SEND · 8f2a8c1e"). */
138
+ export function shortSendId(id: string): string {
139
+ return id.slice(-SHORT_ID_LENGTH)
140
+ }
141
+
142
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
143
+
144
+ /**
145
+ * Why a corrected address cannot be used, as an i18n key: empty, not an
146
+ * e-mail, or the address that already failed. `null` when it can.
147
+ */
148
+ export function correctedAddressError(
149
+ value: string,
150
+ previous: string,
151
+ ): string | null {
152
+ const address = value.trim()
153
+ const rules: [boolean, string][] = [
154
+ [!address, 'dms_mailing.sends.fix_address.errors.required'],
155
+ [
156
+ !EMAIL_PATTERN.test(address),
157
+ 'dms_mailing.sends.fix_address.errors.invalid',
158
+ ],
159
+ [
160
+ address.toLowerCase() === previous.trim().toLowerCase(),
161
+ 'dms_mailing.sends.fix_address.errors.same',
162
+ ],
163
+ ]
164
+ return rules.find(([failed]) => failed)?.[1] ?? null
165
+ }
166
+
167
+ /**
168
+ * What a send again did: went out, stopped by the suppression list (nothing
169
+ * went out, the original stays a problem), or failed at the provider.
170
+ */
171
+ export type ReplayOutcome = 'sent' | 'suppressed' | 'failed'
172
+
173
+ export function replayOutcome(result: {
174
+ status: string
175
+ error?: string
176
+ }): ReplayOutcome {
177
+ if (isSuppressedStatus(result.status)) return 'suppressed'
178
+ return result.error || result.status === 'failed' ? 'failed' : 'sent'
179
+ }
180
+
181
+ const PAYLOAD_INDENT = 2
182
+
183
+ /** The variables a send carried, pretty-printed; the raw text when unparsable. */
184
+ export function formatPayload(raw: string | undefined): string {
185
+ if (!raw) return ''
186
+ try {
187
+ return JSON.stringify(JSON.parse(raw), null, PAYLOAD_INDENT)
188
+ } catch {
189
+ return raw
190
+ }
191
+ }
192
+
193
+ export type SendTone = 'neutral' | 'info' | 'success' | 'warning' | 'error'
194
+
195
+ /** The tone of a send's status pill, the same in the log and the drawer. */
196
+ export const SEND_STATUS_TONES: Record<SendStatus, SendTone> = {
197
+ queued: 'neutral',
198
+ sent: 'info',
199
+ delivered: 'success',
200
+ opened: 'info',
201
+ clicked: 'info',
202
+ bounced: 'error',
203
+ spam: 'warning',
204
+ failed: 'error',
205
+ unsubscribed: 'warning',
206
+ suppressed: 'neutral',
207
+ }
@@ -0,0 +1,28 @@
1
+ import type { SendResult } from '../composables/useMailingApi'
2
+ import { SENDS_PATH } from './pages'
3
+
4
+ /** The DMS table's deep-link key: the send log opens that send's drawer. */
5
+ const RECORD_QUERY_KEY = 'record'
6
+
7
+ export interface SendLogRoute {
8
+ path: string
9
+ query: Record<string, string>
10
+ }
11
+
12
+ /**
13
+ * Where a send toast's "View in the send log" leads: the send itself when one
14
+ * went out, the log otherwise.
15
+ */
16
+ export function sendLogRoute(results: SendResult[]): SendLogRoute {
17
+ const sent = results.filter((result) => !result.error && result.sendId)
18
+ const only = sent.length === 1 ? sent[0] : undefined
19
+ return {
20
+ path: SENDS_PATH,
21
+ query: only ? { [RECORD_QUERY_KEY]: only.sendId } : {},
22
+ }
23
+ }
24
+
25
+ /** One send of the log, its drawer open. */
26
+ export function sendRoute(sendId: string): SendLogRoute {
27
+ return { path: SENDS_PATH, query: { [RECORD_QUERY_KEY]: sendId } }
28
+ }
@@ -0,0 +1,97 @@
1
+ import { readPath } from './paths'
2
+ import type { VariableType } from '../types/mailing'
3
+
4
+ export interface SnippetVariable {
5
+ path: string
6
+ type: VariableType
7
+ }
8
+
9
+ export interface SendSnippetInput {
10
+ slug: string
11
+ locale: string
12
+ /**
13
+ * The paths the content uses, declared or only detected, and the system
14
+ * URLs its footer links need (`utils/system-variables.ts`).
15
+ */
16
+ variables: SnippetVariable[]
17
+ /** The template's test data: example values where it has them. */
18
+ testData: Record<string, unknown>
19
+ }
20
+
21
+ type VariableTree = Record<string, unknown>
22
+
23
+ const EXAMPLE_VALUES: Record<VariableType, unknown> = {
24
+ string: '…',
25
+ text: '…',
26
+ number: 0,
27
+ money: 0,
28
+ date: '2026-01-31',
29
+ url: 'https://…',
30
+ boolean: true,
31
+ array: [],
32
+ }
33
+
34
+ const ARRAY_SUFFIX = '[]'
35
+ const PATH_SEPARATOR = '.'
36
+ const INDENT = ' '
37
+ const JSON_INDENT = 2
38
+ const QUOTED_IDENTIFIER_KEY = /"([A-Za-z_$][\w$]*)":/g
39
+
40
+ const isTree = (value: unknown): value is VariableTree =>
41
+ value !== null && typeof value === 'object' && !Array.isArray(value)
42
+
43
+ /** A list item path (`order.items[].name`) stands for its list. */
44
+ function rootVariable(variable: SnippetVariable): SnippetVariable {
45
+ const at = variable.path.indexOf(ARRAY_SUFFIX)
46
+ return at < 0 ? variable : { path: variable.path.slice(0, at), type: 'array' }
47
+ }
48
+
49
+ /** Sets `value` at a dotted path, never over a branch a deeper path built. */
50
+ function setPath(tree: VariableTree, path: string, value: unknown): void {
51
+ const keys = path.split(PATH_SEPARATOR)
52
+ const leaf = keys.pop() as string
53
+ const parent = keys.reduce<VariableTree>((node, key) => {
54
+ if (!isTree(node[key])) node[key] = {}
55
+ return node[key] as VariableTree
56
+ }, tree)
57
+ if (!isTree(parent[leaf])) parent[leaf] = value
58
+ }
59
+
60
+ /** The variables a send passes, as one object of example values. */
61
+ export function exampleVariables(input: SendSnippetInput): VariableTree {
62
+ const tree: VariableTree = {}
63
+ input.variables
64
+ .map(rootVariable)
65
+ .filter((variable) => variable.path)
66
+ .forEach((variable) => {
67
+ const example = readPath(input.testData, variable.path)
68
+ setPath(
69
+ tree,
70
+ variable.path,
71
+ example ?? EXAMPLE_VALUES[variable.type] ?? EXAMPLE_VALUES.string,
72
+ )
73
+ })
74
+ return tree
75
+ }
76
+
77
+ function literal(value: unknown): string {
78
+ return JSON.stringify(value, null, JSON_INDENT)
79
+ .replace(QUOTED_IDENTIFIER_KEY, '$1:')
80
+ .split('\n')
81
+ .join(`\n${INDENT}`)
82
+ }
83
+
84
+ /**
85
+ * The `SendTemplate` call that sends this template from code: every
86
+ * parameter the interface requires, and the variables its content uses.
87
+ */
88
+ export function sendSnippet(input: SendSnippetInput): string {
89
+ return [
90
+ `await SendTemplate(${JSON.stringify(input.slug)}, {`,
91
+ `${INDENT}tenantId,`,
92
+ `${INDENT}to: customer.email,`,
93
+ `${INDENT}locale: ${JSON.stringify(input.locale)},`,
94
+ `${INDENT}variables: ${literal(exampleVariables(input))},`,
95
+ '});',
96
+ ].join('\n')
97
+ }
@@ -0,0 +1,24 @@
1
+ /** Past this latency the drawer shows the API call in red. */
2
+ export const SLOW_LATENCY_MS = 5000
3
+
4
+ const MS_PER_SECOND = 1000
5
+ const SECONDS_DECIMALS = 1
6
+
7
+ /** A latency as the log prints it: milliseconds, seconds past 5 s, a dash when unknown. */
8
+ export function formatLatency(
9
+ ms: number | null | undefined,
10
+ locale: string,
11
+ ): string {
12
+ if (ms === null || ms === undefined) return '—'
13
+ if (ms < SLOW_LATENCY_MS)
14
+ return `${new Intl.NumberFormat(locale).format(ms)} ms`
15
+ const seconds = new Intl.NumberFormat(locale, {
16
+ minimumFractionDigits: SECONDS_DECIMALS,
17
+ maximumFractionDigits: SECONDS_DECIMALS,
18
+ }).format(ms / MS_PER_SECOND)
19
+ return `${seconds} s`
20
+ }
21
+
22
+ export function isSlowLatency(ms: number | null | undefined): boolean {
23
+ return (ms ?? 0) >= SLOW_LATENCY_MS
24
+ }