@antelopejs/dms-mailing 0.3.3 → 0.4.0

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 (705) 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 +114 -40
  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 +4 -1
  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 +1 -1
  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 +139 -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 -19
  412. package/frontend-vue/tests/dnd.test.ts +10 -0
  413. package/frontend-vue/tests/editor-block-list.test.ts +24 -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 +206 -0
  417. package/frontend-vue/tests/footer-block.test.ts +88 -0
  418. package/frontend-vue/tests/funnel-card.test.ts +116 -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 +38 -0
  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 +210 -0
  429. package/frontend-vue/tests/overview-gate.test.ts +146 -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 +129 -0
  438. package/frontend-vue/tests/send-actions.test.ts +128 -0
  439. package/frontend-vue/tests/send-modals.test.ts +415 -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 +75 -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 +449 -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 +370 -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 +22 -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/package.json +11 -9
  461. package/src/automation/email-event-trigger.ts +3 -3
  462. package/src/automation/send-template-action.ts +7 -5
  463. package/src/config.ts +3 -1
  464. package/src/constants.ts +18 -2
  465. package/src/crons/attention.ts +21 -0
  466. package/src/crons/index.ts +2 -0
  467. package/src/crons/jobs.ts +35 -0
  468. package/src/crons/retention.ts +7 -22
  469. package/src/data/displays.ts +18 -0
  470. package/src/data/index.ts +1 -0
  471. package/src/data/locale-fallback.ts +26 -0
  472. package/src/data/read-only-routes.ts +14 -0
  473. package/src/data/sends-table.ts +204 -19
  474. package/src/data/suppressions-table.ts +127 -0
  475. package/src/data/templates-table.ts +295 -63
  476. package/src/data-types/categories-type.ts +4 -5
  477. package/src/data-types/category-type.ts +1 -1
  478. package/src/db/models/index.ts +3 -0
  479. package/src/db/models/sends.model.ts +102 -1
  480. package/src/db/models/settings.model.ts +2 -12
  481. package/src/db/models/suppressions.model.ts +53 -0
  482. package/src/db/models/template-versions.model.ts +23 -0
  483. package/src/db/models/templates.model.ts +17 -0
  484. package/src/db/models/webhook-status.model.ts +25 -0
  485. package/src/db/tables/index.ts +3 -0
  486. package/src/db/tables/send-events.table.ts +6 -2
  487. package/src/db/tables/sends.table.ts +37 -2
  488. package/src/db/tables/suppressions.table.ts +46 -0
  489. package/src/db/tables/template-versions.table.ts +44 -0
  490. package/src/db/tables/templates.table.ts +67 -5
  491. package/src/db/tables/webhook-status.table.ts +44 -0
  492. package/src/engine/changes.ts +198 -0
  493. package/src/engine/conditions.ts +1 -7
  494. package/src/engine/coverage.ts +7 -2
  495. package/src/engine/fallback-html.ts +80 -42
  496. package/src/engine/index.ts +3 -0
  497. package/src/engine/plain-text.ts +86 -0
  498. package/src/engine/resolve.ts +49 -15
  499. package/src/engine/system-variables.ts +124 -0
  500. package/src/engine/tokens.ts +13 -1
  501. package/src/hooks/tenant-export.ts +21 -2
  502. package/src/implementations/dms-mailing/index.ts +1 -1
  503. package/src/index.ts +29 -13
  504. package/src/pages/editor.ts +13 -2
  505. package/src/pages/index.ts +6 -3
  506. package/src/pages/module.ts +31 -2
  507. package/src/pages/overview.ts +187 -128
  508. package/src/pages/sends.ts +271 -112
  509. package/src/pages/settings/form.ts +144 -54
  510. package/src/pages/settings/page.ts +9 -17
  511. package/src/pages/suppressions.ts +157 -0
  512. package/src/pages/templates.ts +139 -42
  513. package/src/permissions.ts +6 -4
  514. package/src/quick-actions.ts +12 -5
  515. package/src/routes/events.ts +110 -33
  516. package/src/routes/index.ts +2 -0
  517. package/src/routes/metrics.ts +348 -98
  518. package/src/routes/period.ts +23 -6
  519. package/src/routes/sends.ts +413 -41
  520. package/src/routes/settings.ts +291 -26
  521. package/src/routes/suppressions.ts +87 -0
  522. package/src/routes/template-versions.ts +116 -0
  523. package/src/routes/templates.ts +334 -48
  524. package/src/services/attention.ts +283 -30
  525. package/src/services/catalog.ts +28 -0
  526. package/src/services/categories.ts +28 -27
  527. package/src/services/checked-send.ts +123 -0
  528. package/src/services/events.ts +174 -20
  529. package/src/services/metrics.ts +137 -28
  530. package/src/services/notify.ts +126 -44
  531. package/src/services/provider-banner.ts +165 -0
  532. package/src/services/render.ts +1 -0
  533. package/src/services/send-stat-items.ts +160 -0
  534. package/src/services/send-stats.ts +122 -0
  535. package/src/services/send.ts +193 -31
  536. package/src/services/settings.ts +52 -8
  537. package/src/services/starters.ts +383 -0
  538. package/src/services/suppressions.ts +116 -0
  539. package/src/services/template-attention.ts +82 -0
  540. package/src/services/template-stats.ts +97 -0
  541. package/src/services/templates.ts +400 -37
  542. package/src/services/webhook-status.ts +202 -0
  543. package/src/types/blocks.ts +6 -0
  544. package/src/types/index.ts +2 -0
  545. package/src/types/sends.ts +64 -2
  546. package/src/types/suppressions.ts +27 -0
  547. package/src/types/webhook.ts +29 -0
  548. package/src/validation/settings.schema.ts +173 -17
  549. package/src/validation/templates.schema.ts +53 -1
  550. package/dist/antelope-modules.d.ts +0 -32
  551. package/dist/antelope-modules.js +0 -36
  552. package/dist/antelope-modules.js.map +0 -1
  553. package/dist/test/antelope.test.d.ts +0 -2
  554. package/dist/test/antelope.test.js +0 -180
  555. package/dist/test/antelope.test.js.map +0 -1
  556. package/dist/test/helpers/http.d.ts +0 -4
  557. package/dist/test/helpers/http.js +0 -28
  558. package/dist/test/helpers/http.js.map +0 -1
  559. package/dist/test/helpers/owner.d.ts +0 -5
  560. package/dist/test/helpers/owner.js +0 -30
  561. package/dist/test/helpers/owner.js.map +0 -1
  562. package/dist/test/integration/events.test.d.ts +0 -1
  563. package/dist/test/integration/events.test.js +0 -143
  564. package/dist/test/integration/events.test.js.map +0 -1
  565. package/dist/test/integration/retention.test.d.ts +0 -1
  566. package/dist/test/integration/retention.test.js +0 -291
  567. package/dist/test/integration/retention.test.js.map +0 -1
  568. package/dist/test/integration/sends.test.d.ts +0 -1
  569. package/dist/test/integration/sends.test.js +0 -210
  570. package/dist/test/integration/sends.test.js.map +0 -1
  571. package/dist/test/integration/settings.test.d.ts +0 -1
  572. package/dist/test/integration/settings.test.js +0 -137
  573. package/dist/test/integration/settings.test.js.map +0 -1
  574. package/dist/test/integration/setup.test.d.ts +0 -1
  575. package/dist/test/integration/setup.test.js +0 -32
  576. package/dist/test/integration/setup.test.js.map +0 -1
  577. package/dist/test/integration/templates.test.d.ts +0 -1
  578. package/dist/test/integration/templates.test.js +0 -233
  579. package/dist/test/integration/templates.test.js.map +0 -1
  580. package/dist/test/integration/tenant-export.test.d.ts +0 -1
  581. package/dist/test/integration/tenant-export.test.js +0 -85
  582. package/dist/test/integration/tenant-export.test.js.map +0 -1
  583. package/dist/test/unit/attention.test.d.ts +0 -1
  584. package/dist/test/unit/attention.test.js +0 -109
  585. package/dist/test/unit/attention.test.js.map +0 -1
  586. package/dist/test/unit/automation.test.d.ts +0 -1
  587. package/dist/test/unit/automation.test.js +0 -104
  588. package/dist/test/unit/automation.test.js.map +0 -1
  589. package/dist/test/unit/categories.test.d.ts +0 -1
  590. package/dist/test/unit/categories.test.js +0 -38
  591. package/dist/test/unit/categories.test.js.map +0 -1
  592. package/dist/test/unit/engine/conditions.test.d.ts +0 -1
  593. package/dist/test/unit/engine/conditions.test.js +0 -23
  594. package/dist/test/unit/engine/conditions.test.js.map +0 -1
  595. package/dist/test/unit/engine/coverage.test.d.ts +0 -1
  596. package/dist/test/unit/engine/coverage.test.js +0 -106
  597. package/dist/test/unit/engine/coverage.test.js.map +0 -1
  598. package/dist/test/unit/engine/fallback-html.test.d.ts +0 -1
  599. package/dist/test/unit/engine/fallback-html.test.js +0 -78
  600. package/dist/test/unit/engine/fallback-html.test.js.map +0 -1
  601. package/dist/test/unit/engine/paths.test.d.ts +0 -1
  602. package/dist/test/unit/engine/paths.test.js +0 -33
  603. package/dist/test/unit/engine/paths.test.js.map +0 -1
  604. package/dist/test/unit/engine/resolve.test.d.ts +0 -1
  605. package/dist/test/unit/engine/resolve.test.js +0 -157
  606. package/dist/test/unit/engine/resolve.test.js.map +0 -1
  607. package/dist/test/unit/engine/tokens.test.d.ts +0 -1
  608. package/dist/test/unit/engine/tokens.test.js +0 -26
  609. package/dist/test/unit/engine/tokens.test.js.map +0 -1
  610. package/dist/test/unit/events.test.d.ts +0 -1
  611. package/dist/test/unit/events.test.js +0 -13
  612. package/dist/test/unit/events.test.js.map +0 -1
  613. package/dist/test/unit/metrics.test.d.ts +0 -1
  614. package/dist/test/unit/metrics.test.js +0 -108
  615. package/dist/test/unit/metrics.test.js.map +0 -1
  616. package/dist/test/unit/notify.test.d.ts +0 -1
  617. package/dist/test/unit/notify.test.js +0 -49
  618. package/dist/test/unit/notify.test.js.map +0 -1
  619. package/dist/test/unit/period.test.d.ts +0 -1
  620. package/dist/test/unit/period.test.js +0 -61
  621. package/dist/test/unit/period.test.js.map +0 -1
  622. package/dist/test/unit/permissions.test.d.ts +0 -1
  623. package/dist/test/unit/permissions.test.js +0 -27
  624. package/dist/test/unit/permissions.test.js.map +0 -1
  625. package/dist/test/unit/provider.test.d.ts +0 -1
  626. package/dist/test/unit/provider.test.js +0 -47
  627. package/dist/test/unit/provider.test.js.map +0 -1
  628. package/dist/test/unit/realtime.test.d.ts +0 -1
  629. package/dist/test/unit/realtime.test.js +0 -13
  630. package/dist/test/unit/realtime.test.js.map +0 -1
  631. package/dist/test/unit/render.test.d.ts +0 -1
  632. package/dist/test/unit/render.test.js +0 -23
  633. package/dist/test/unit/render.test.js.map +0 -1
  634. package/dist/test/unit/retention.test.d.ts +0 -1
  635. package/dist/test/unit/retention.test.js +0 -66
  636. package/dist/test/unit/retention.test.js.map +0 -1
  637. package/dist/test/unit/send-batch.test.d.ts +0 -1
  638. package/dist/test/unit/send-batch.test.js +0 -46
  639. package/dist/test/unit/send-batch.test.js.map +0 -1
  640. package/dist/test/unit/send.test.d.ts +0 -1
  641. package/dist/test/unit/send.test.js +0 -120
  642. package/dist/test/unit/send.test.js.map +0 -1
  643. package/dist/test/unit/smoke.test.d.ts +0 -1
  644. package/dist/test/unit/smoke.test.js +0 -10
  645. package/dist/test/unit/smoke.test.js.map +0 -1
  646. package/dist/test/unit/status.test.d.ts +0 -1
  647. package/dist/test/unit/status.test.js +0 -16
  648. package/dist/test/unit/status.test.js.map +0 -1
  649. package/dist/test/unit/templates.test.d.ts +0 -1
  650. package/dist/test/unit/templates.test.js +0 -48
  651. package/dist/test/unit/templates.test.js.map +0 -1
  652. package/dist/test/unit/tenant-export.test.d.ts +0 -1
  653. package/dist/test/unit/tenant-export.test.js +0 -59
  654. package/dist/test/unit/tenant-export.test.js.map +0 -1
  655. package/dist/test/unit/webhook-secret.test.d.ts +0 -1
  656. package/dist/test/unit/webhook-secret.test.js +0 -43
  657. package/dist/test/unit/webhook-secret.test.js.map +0 -1
  658. package/frontend-vue/app/components/AttentionCard.vue +0 -78
  659. package/frontend-vue/app/components/HtmlPreviewModal.vue +0 -20
  660. package/frontend-vue/app/components/ProviderChip.vue +0 -39
  661. package/frontend-vue/app/components/editor/EditorBlockSettings.vue +0 -186
  662. package/frontend-vue/app/components/editor/EditorDataPanel.vue +0 -130
  663. package/frontend-vue/app/components/editor/EditorDraftBar.vue +0 -35
  664. package/frontend-vue/app/components/editor/EditorRail.vue +0 -58
  665. package/frontend-vue/app/components/editor/EditorSubjectSettings.vue +0 -37
  666. package/frontend-vue/app/components/editor/EditorTemplateSettings.vue +0 -87
  667. package/frontend-vue/app/components/editor/EditorTestSendModal.vue +0 -66
  668. package/frontend-vue/app/components/editor/EditorVariables.vue +0 -163
  669. package/frontend-vue/app/composables/useMailingPeriodData.ts +0 -39
  670. package/src/antelope-modules.ts +0 -48
  671. package/src/test/antelope.test.ts +0 -190
  672. package/src/test/helpers/http.ts +0 -23
  673. package/src/test/helpers/owner.ts +0 -37
  674. package/src/test/integration/events.test.ts +0 -159
  675. package/src/test/integration/retention.test.ts +0 -363
  676. package/src/test/integration/sends.test.ts +0 -233
  677. package/src/test/integration/settings.test.ts +0 -128
  678. package/src/test/integration/setup.test.ts +0 -35
  679. package/src/test/integration/templates.test.ts +0 -272
  680. package/src/test/integration/tenant-export.test.ts +0 -125
  681. package/src/test/unit/attention.test.ts +0 -129
  682. package/src/test/unit/automation.test.ts +0 -130
  683. package/src/test/unit/categories.test.ts +0 -51
  684. package/src/test/unit/engine/conditions.test.ts +0 -35
  685. package/src/test/unit/engine/coverage.test.ts +0 -118
  686. package/src/test/unit/engine/fallback-html.test.ts +0 -86
  687. package/src/test/unit/engine/paths.test.ts +0 -39
  688. package/src/test/unit/engine/resolve.test.ts +0 -176
  689. package/src/test/unit/engine/tokens.test.ts +0 -26
  690. package/src/test/unit/events.test.ts +0 -19
  691. package/src/test/unit/metrics.test.ts +0 -170
  692. package/src/test/unit/notify.test.ts +0 -71
  693. package/src/test/unit/period.test.ts +0 -91
  694. package/src/test/unit/permissions.test.ts +0 -30
  695. package/src/test/unit/provider.test.ts +0 -57
  696. package/src/test/unit/realtime.test.ts +0 -11
  697. package/src/test/unit/render.test.ts +0 -23
  698. package/src/test/unit/retention.test.ts +0 -91
  699. package/src/test/unit/send-batch.test.ts +0 -72
  700. package/src/test/unit/send.test.ts +0 -161
  701. package/src/test/unit/smoke.test.ts +0 -8
  702. package/src/test/unit/status.test.ts +0 -14
  703. package/src/test/unit/templates.test.ts +0 -52
  704. package/src/test/unit/tenant-export.test.ts +0 -88
  705. package/src/test/unit/webhook-secret.test.ts +0 -51
@@ -1,14 +1,21 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
+ import { Logging } from "@antelopejs/interface-core/logging";
2
3
  import { GetModel } from "@antelopejs/interface-database-decorators";
3
4
  import {
4
5
  emitEmailEvent,
5
6
  type EmailEventPayload,
6
7
  } from "../automation/email-event-trigger";
7
- import { type MailingSend, SendEventModel, SendModel } from "../db";
8
+ import {
9
+ type MailingSend,
10
+ type MailingSendEvent,
11
+ SendEventModel,
12
+ SendModel,
13
+ } from "../db";
8
14
  import type { EmailEvent } from "@antelopejs/interface-dms-mailing";
9
15
  import { publishSendUpdated } from "../realtime";
10
16
  import type { SendEventType, SendStatus } from "../types";
11
- import { notifyBounce } from "./notify";
17
+ import { notifyProblems } from "./notify";
18
+ import { suppressAddress, suppressionReasonOf } from "./suppressions";
12
19
 
13
20
  export interface SendCounters {
14
21
  status: SendStatus;
@@ -28,6 +35,7 @@ const RANK: Record<SendStatus, number> = {
28
35
  spam: 6,
29
36
  bounced: 7,
30
37
  failed: 7,
38
+ suppressed: 8,
31
39
  };
32
40
 
33
41
  const COUNTERS: Partial<Record<SendEventType, CounterKey>> = {
@@ -37,6 +45,40 @@ const COUNTERS: Partial<Record<SendEventType, CounterKey>> = {
37
45
 
38
46
  const MAX_ACTIVITY_ATTEMPTS = 5;
39
47
 
48
+ const PROBLEM_EVENTS = new Set<SendStatus>(["bounced", "failed", "spam"]);
49
+
50
+ /**
51
+ * Statuses a later sign of delivery overrides: a provider failure reported by
52
+ * webhook (`421 4.3.0`) is often a deferral the provider retried.
53
+ */
54
+ const DEFERRABLE_STATUSES = new Set<SendStatus>(["failed"]);
55
+ const CLEARED_ERROR = "";
56
+
57
+ const SOFT_BOUNCE_TYPE = /soft|transient|temporary/i;
58
+ const HARD_BOUNCE_TYPE = /hard|permanent/i;
59
+ /** A temporary SMTP answer: a `4xx` reply code or a `4.x.x` enhanced status. */
60
+ const SOFT_SMTP_ANSWER = /^\s*4\d\d\b|\b4\.\d{1,3}\.\d{1,3}\b/;
61
+
62
+ const textOf = (value: unknown): string =>
63
+ typeof value === "string" ? value : "";
64
+
65
+ /**
66
+ * The provider's own words for a problem event (`550 5.1.1 Mailbox does not
67
+ * exist`), kept on the send so the log shows why it failed. Other events
68
+ * leave the stored error alone.
69
+ */
70
+ export function problemReason(event: EmailEvent): string | undefined {
71
+ if (!PROBLEM_EVENTS.has(event.type)) return undefined;
72
+ const reason = event.details?.reason;
73
+ return typeof reason === "string" && reason ? reason : undefined;
74
+ }
75
+
76
+ function overridesStatus(current: SendStatus, next: SendStatus): boolean {
77
+ if (DEFERRABLE_STATUSES.has(current))
78
+ return next !== current && RANK[next] >= RANK.delivered;
79
+ return RANK[next] > RANK[current];
80
+ }
81
+
40
82
  export function applyEvent(
41
83
  current: SendCounters,
42
84
  type: SendEventType,
@@ -44,14 +86,101 @@ export function applyEvent(
44
86
  const counter = COUNTERS[type];
45
87
  const next: SendCounters = { ...current };
46
88
  if (counter) next[counter] = current[counter] + 1;
47
- if (RANK[type] > RANK[current.status]) next.status = type;
89
+ if (overridesStatus(current.status, type)) next.status = type;
48
90
  return next;
49
91
  }
50
92
 
51
- async function publishEmailEvent(payload: EmailEventPayload): Promise<void> {
93
+ /**
94
+ * Whether a bounce is temporary (a full mailbox, a greylisting): the
95
+ * provider's `bounceType` when it gives one, else the SMTP answer's class.
96
+ */
97
+ export function isSoftBounce(event: EmailEvent): boolean {
98
+ const bounceType = textOf(event.details?.bounceType);
99
+ if (SOFT_BOUNCE_TYPE.test(bounceType)) return true;
100
+ if (HARD_BOUNCE_TYPE.test(bounceType)) return false;
101
+ return SOFT_SMTP_ANSWER.test(textOf(event.details?.reason));
102
+ }
103
+
104
+ /**
105
+ * The error the send keeps after an event: the provider's words for a
106
+ * problem, none once a deferred failure turns out delivered, else unchanged.
107
+ */
108
+ export function errorAfter(
109
+ event: EmailEvent,
110
+ previous: SendStatus,
111
+ next: SendStatus,
112
+ ): string | undefined {
113
+ const reason = problemReason(event);
114
+ if (reason) return reason;
115
+ const recovered = DEFERRABLE_STATUSES.has(previous) && previous !== next;
116
+ return recovered && !PROBLEM_EVENTS.has(next) ? CLEARED_ERROR : undefined;
117
+ }
118
+
119
+ function activityOf(send: MailingSend, event: EmailEvent) {
120
+ const counters = applyEvent(
121
+ { status: send.status, opens: send.opens, clicks: send.clicks },
122
+ event.type,
123
+ );
124
+ return {
125
+ ...counters,
126
+ lastEventAt: event.at ?? new Date(),
127
+ provider: event.provider,
128
+ error: errorAfter(event, send.status, counters.status),
129
+ isSoftBounce: event.type === "bounced" ? isSoftBounce(event) : undefined,
130
+ };
131
+ }
132
+
133
+ async function publishEmailEvent(
134
+ payload: EmailEventPayload,
135
+ send: MailingSend,
136
+ ): Promise<void> {
52
137
  emitEmailEvent(payload);
53
138
  publishSendUpdated(payload.sendId);
54
- await notifyBounce(payload.tenantId, payload);
139
+ await notifyProblems(payload.tenantId, {
140
+ sendId: send._id,
141
+ templateSlug: send.templateSlug,
142
+ type: payload.type,
143
+ isTest: send.isTest,
144
+ });
145
+ }
146
+
147
+ const eventIdOf = (details: Record<string, unknown> | undefined): string =>
148
+ textOf(details?.eventId);
149
+
150
+ /**
151
+ * Whether a stored event is the one a webhook delivers again: the same
152
+ * provider event id, or the same type, time and details when it has none.
153
+ */
154
+ export function isSameEvent(
155
+ stored: MailingSendEvent,
156
+ event: EmailEvent,
157
+ ): boolean {
158
+ const eventId = eventIdOf(event.details);
159
+ if (eventId) {
160
+ const storedDetails = JSON.parse(stored.json_details || "{}") as Record<
161
+ string,
162
+ unknown
163
+ >;
164
+ return eventIdOf(storedDetails) === eventId;
165
+ }
166
+ if (!event.at || stored.type !== event.type) return false;
167
+ return (
168
+ new Date(stored.at).getTime() === event.at.getTime() &&
169
+ stored.json_details === JSON.stringify(event.details ?? {})
170
+ );
171
+ }
172
+
173
+ /** A provider retrying its webhook must not count the event twice. */
174
+ async function isRepeatedEvent(
175
+ tenantId: string,
176
+ event: EmailEvent,
177
+ ): Promise<boolean> {
178
+ const send = await GetModel(SendModel, tenantId).getByProviderMessageId(
179
+ event.messageId,
180
+ );
181
+ if (!send) return false;
182
+ const stored = await GetModel(SendEventModel, tenantId).getBySend(send._id);
183
+ return stored.some((candidate) => isSameEvent(candidate, event));
55
184
  }
56
185
 
57
186
  async function acceptEmailActivity(
@@ -62,23 +191,44 @@ async function acceptEmailActivity(
62
191
  for (let attempt = 0; attempt < MAX_ACTIVITY_ATTEMPTS; attempt++) {
63
192
  const send = await sends.getByProviderMessageId(event.messageId);
64
193
  if (!send || send.isRetiring) return undefined;
65
- const accepted = await sends.recordActivity(send, {
66
- ...applyEvent(
67
- { status: send.status, opens: send.opens, clicks: send.clicks },
68
- event.type,
69
- ),
70
- lastEventAt: event.at ?? new Date(),
71
- provider: event.provider,
72
- });
194
+ const accepted = await sends.recordActivity(send, activityOf(send, event));
73
195
  if (accepted) return send;
74
196
  }
75
197
  throw new Error("Mailing activity conflicted repeatedly; retry the webhook");
76
198
  }
77
199
 
200
+ /**
201
+ * Puts the send's address on the suppression list when the event says
202
+ * nothing more must reach it. The event is recorded whatever happens here.
203
+ */
204
+ async function suppressAfterEvent(
205
+ tenantId: string,
206
+ send: MailingSend,
207
+ event: EmailEvent,
208
+ ): Promise<void> {
209
+ const reason = suppressionReasonOf({
210
+ type: event.type,
211
+ isTest: send.isTest,
212
+ isSoftBounce: event.type === "bounced" && isSoftBounce(event),
213
+ });
214
+ if (!reason) return;
215
+ try {
216
+ await suppressAddress(tenantId, {
217
+ email: send.recipientEmail,
218
+ reason,
219
+ sendId: send._id,
220
+ detail: textOf(event.details?.reason) || undefined,
221
+ });
222
+ } catch (error) {
223
+ Logging.Error("[dms-mailing] Could not suppress an address:", error);
224
+ }
225
+ }
226
+
78
227
  export async function recordEmailEvent(
79
228
  tenantId: string,
80
229
  event: EmailEvent,
81
230
  ): Promise<boolean> {
231
+ if (await isRepeatedEvent(tenantId, event)) return false;
82
232
  const send = await acceptEmailActivity(tenantId, event);
83
233
  if (!send) return false;
84
234
  const at = event.at ?? new Date();
@@ -91,13 +241,17 @@ export async function recordEmailEvent(
91
241
  },
92
242
  );
93
243
  if (!recorded) return false;
94
- await publishEmailEvent({
95
- tenantId,
96
- sendId: send._id,
97
- templateSlug: send.templateSlug,
98
- type: event.type,
99
- recipientEmail: send.recipientEmail,
100
- });
244
+ await suppressAfterEvent(tenantId, send, event);
245
+ await publishEmailEvent(
246
+ {
247
+ tenantId,
248
+ sendId: send._id,
249
+ templateSlug: send.templateSlug,
250
+ type: event.type,
251
+ recipientEmail: send.recipientEmail,
252
+ },
253
+ send,
254
+ );
101
255
  return true;
102
256
  }
103
257
 
@@ -21,6 +21,13 @@ export function readAudience(raw: string | null | undefined): SendAudience {
21
21
  return AUDIENCE_BY_NAME[raw ?? ""] ?? BUSINESS_AUDIENCE;
22
22
  }
23
23
 
24
+ const STATS_WINDOW_DAYS = 30;
25
+
26
+ /** Where the per-template figures start: the last 30 days. */
27
+ export function statsWindowStart(now: number = Date.now()): Date {
28
+ return new Date(now - STATS_WINDOW_DAYS * DAY_MS);
29
+ }
30
+
24
31
  export function forAudience(
25
32
  sends: SendSummary[],
26
33
  audience: SendAudience,
@@ -28,6 +35,10 @@ export function forAudience(
28
35
  return sends.filter(AUDIENCE_KEEPS[audience]);
29
36
  }
30
37
 
38
+ /**
39
+ * The counters of a set of sends and the rates drawn from them. A rate is
40
+ * `null` when its base is empty: "no e-mail delivered" is not "0 % opened".
41
+ */
31
42
  export interface Totals {
32
43
  sends: number;
33
44
  queued: number;
@@ -39,12 +50,15 @@ export interface Totals {
39
50
  failed: number;
40
51
  spam: number;
41
52
  unsubscribed: number;
42
- deliverability: number;
43
- openRate: number;
44
- clickRate: number;
53
+ deliverability: number | null;
54
+ openRate: number | null;
55
+ clickRate: number | null;
45
56
  latencyMedian: number;
46
57
  }
47
58
 
59
+ /** Reads one figure out of the totals; `null` when it cannot be stated. */
60
+ export type TotalsExtractor = (totals: Totals) => number | null;
61
+
48
62
  export interface SeriesPoint {
49
63
  x: string;
50
64
  y: number;
@@ -55,6 +69,13 @@ export interface DateRange {
55
69
  to: Date;
56
70
  }
57
71
 
72
+ /** The provider features the Overview's figures depend on. */
73
+ export interface ProviderTracking {
74
+ name: string;
75
+ openTracking: boolean;
76
+ clickTracking: boolean;
77
+ }
78
+
58
79
  const PERCENT = 100;
59
80
  const DAY_MS = 86_400_000;
60
81
  const DAY_KEY_LENGTH = 10;
@@ -67,9 +88,11 @@ const DELIVERED_LIKE: SendStatus[] = [
67
88
  "spam",
68
89
  ];
69
90
  const OPENED_LIKE: SendStatus[] = ["opened", "clicked"];
91
+ /** Statuses only an event reported by the provider can set. */
92
+ const PROVIDER_EVENT_STATUSES: SendStatus[] = [...DELIVERED_LIKE, "bounced"];
70
93
 
71
- export const ratio = (part: number, whole: number): number =>
72
- whole ? (part / whole) * PERCENT : 0;
94
+ export const ratio = (part: number, whole: number): number | null =>
95
+ whole ? (part / whole) * PERCENT : null;
73
96
 
74
97
  const countIf = (
75
98
  sends: SendSummary[],
@@ -81,18 +104,36 @@ const byStatus =
81
104
  (send: SendSummary): boolean =>
82
105
  send.status === status;
83
106
 
107
+ const isDelivered = (send: SendSummary): boolean =>
108
+ DELIVERED_LIKE.includes(send.status);
109
+
110
+ /**
111
+ * Engagement outlives the final status: a send opened, clicked and then
112
+ * unsubscribed (or reported as spam) was still opened and clicked, and a
113
+ * click implies an open the pixel may have missed.
114
+ */
115
+ const wasClicked = (send: SendSummary): boolean =>
116
+ isDelivered(send) && (send.status === "clicked" || send.clicks > 0);
117
+
118
+ const wasOpened = (send: SendSummary): boolean =>
119
+ isDelivered(send) &&
120
+ (OPENED_LIKE.includes(send.status) || send.opens > 0 || send.clicks > 0);
121
+
84
122
  function median(values: number[]): number {
85
123
  if (!values.length) return 0;
86
124
  const sorted = [...values].sort((left, right) => left - right);
87
125
  return sorted[Math.floor(sorted.length / 2)] as number;
88
126
  }
89
127
 
90
- export function aggregate(sends: SendSummary[]): Totals {
91
- const delivered = countIf(sends, (send) =>
92
- DELIVERED_LIKE.includes(send.status),
93
- );
94
- const opened = countIf(sends, (send) => OPENED_LIKE.includes(send.status));
95
- const clicked = countIf(sends, byStatus("clicked"));
128
+ /** A send to a suppressed address never left: no figure counts it. */
129
+ export const leftForProvider = (send: SendSummary): boolean =>
130
+ send.status !== "suppressed";
131
+
132
+ export function aggregate(logged: SendSummary[]): Totals {
133
+ const sends = logged.filter(leftForProvider);
134
+ const delivered = countIf(sends, isDelivered);
135
+ const opened = countIf(sends, wasOpened);
136
+ const clicked = countIf(sends, wasClicked);
96
137
  return {
97
138
  sends: sends.length,
98
139
  queued: countIf(sends, byStatus("queued")),
@@ -107,22 +148,55 @@ export function aggregate(sends: SendSummary[]): Totals {
107
148
  deliverability: ratio(delivered, sends.length),
108
149
  openRate: ratio(opened, delivered),
109
150
  clickRate: ratio(clicked, delivered),
110
- latencyMedian: median(sends.map((send) => send.latencyMs)),
151
+ latencyMedian: median(sends.flatMap((send) => send.latencyMs ?? [])),
111
152
  };
112
153
  }
113
154
 
114
- export function deltaPercent(current: number, previous: number): number {
115
- if (!previous) return 0;
155
+ /**
156
+ * The change from `previous` to `current`, in percent; `null` when no share
157
+ * can be stated, a preceding zero included: growth away from nothing is
158
+ * unbounded, and a "0 %" stand-in would read as "no change".
159
+ */
160
+ export function deltaPercent(
161
+ current: number | null,
162
+ previous: number | null,
163
+ ): number | null {
164
+ if (current === null || !previous) return null;
116
165
  return Number(
117
166
  (((current - previous) / previous) * PERCENT).toFixed(DELTA_PRECISION),
118
167
  );
119
168
  }
120
169
 
170
+ const showsProviderEvent = (send: SendSummary): boolean =>
171
+ PROVIDER_EVENT_STATUSES.includes(send.status) ||
172
+ send.opens > 0 ||
173
+ send.clicks > 0;
174
+
175
+ /**
176
+ * Whether the window's delivery figures are unmeasured: the provider tracks
177
+ * nothing itself (SMTP) and no event came back by webhook either, so every
178
+ * send stays "sent" and a 0 % deliverability would be a false alarm. Unknown
179
+ * capabilities (the provider unreachable) claim nothing.
180
+ */
181
+ export function isDeliveryUntracked(
182
+ sends: SendSummary[],
183
+ tracking: ProviderTracking | null,
184
+ ): boolean {
185
+ if (!tracking || tracking.openTracking || tracking.clickTracking)
186
+ return false;
187
+ return sends.length > 0 && !sends.some(showsProviderEvent);
188
+ }
189
+
121
190
  const dayKey = (date: Date): string =>
122
191
  date.toISOString().slice(0, DAY_KEY_LENGTH);
123
192
 
193
+ interface DayBucket {
194
+ x: string;
195
+ sends: SendSummary[];
196
+ }
197
+
124
198
  /**
125
- * One point per day of the window, counted from the window's own boundaries.
199
+ * One bucket per day of the window, counted from the window's own boundaries.
126
200
  *
127
201
  * The DMS period selector cuts on *local* midnights, so a UTC+2 tenant asks for
128
202
  * 2026-09-03T22:00Z → 2026-09-10T21:59:59.999Z. Buckets are therefore offsets
@@ -137,24 +211,59 @@ const dayKey = (date: Date): string =>
137
211
  * to drop it, so a send made today was missing from the chart while the KPI
138
212
  * still counted it.
139
213
  */
140
- export function dailySeries(
141
- sends: SendSummary[],
142
- from: Date,
143
- to: Date,
144
- ): SeriesPoint[] {
214
+ function dailyBuckets(sends: SendSummary[], from: Date, to: Date): DayBucket[] {
145
215
  const start = from.getTime();
146
- const span = to.getTime() - start;
147
- const dayCount = Math.max(1, Math.ceil(span / DAY_MS));
148
- const counts: number[] = Array.from({ length: dayCount }, () => 0);
216
+ const dayCount = Math.max(1, Math.ceil((to.getTime() - start) / DAY_MS));
217
+ const buckets: DayBucket[] = Array.from({ length: dayCount }, (_, index) => ({
218
+ x: dayKey(new Date(start + index * DAY_MS + DAY_MS / 2)),
219
+ sends: [],
220
+ }));
149
221
  for (const send of sends) {
150
222
  const offset = new Date(send.createdAt).getTime() - start;
151
223
  if (offset < 0 || offset >= dayCount * DAY_MS) continue;
152
- const index = Math.floor(offset / DAY_MS);
153
- counts[index] = (counts[index] ?? 0) + 1;
224
+ buckets[Math.floor(offset / DAY_MS)]?.sends.push(send);
154
225
  }
155
- return counts.map((y, index) => ({
156
- x: dayKey(new Date(start + index * DAY_MS + DAY_MS / 2)),
157
- y,
226
+ return buckets;
227
+ }
228
+
229
+ /** The number of sends of each day of the window. */
230
+ export function dailySeries(
231
+ sends: SendSummary[],
232
+ from: Date,
233
+ to: Date,
234
+ ): SeriesPoint[] {
235
+ return dailyBuckets(sends, from, to).map((bucket) => ({
236
+ x: bucket.x,
237
+ y: bucket.sends.length,
238
+ }));
239
+ }
240
+
241
+ /**
242
+ * One figure's own value for each day of the window, the trend a KPI's
243
+ * sparkline draws. A day the figure cannot be stated on (a rate without
244
+ * deliveries) is left out rather than drawn as a fall to zero.
245
+ */
246
+ export function dailyValues(
247
+ sends: SendSummary[],
248
+ range: DateRange,
249
+ extract: TotalsExtractor,
250
+ ): number[] {
251
+ return dailyBuckets(sends, range.from, range.to)
252
+ .map((bucket) => extract(aggregate(bucket.sends)))
253
+ .filter((value): value is number => value !== null);
254
+ }
255
+
256
+ /**
257
+ * Lays a comparison series on the current one's axis, day 1 on day 1, so the
258
+ * two periods overlap instead of standing side by side.
259
+ */
260
+ export function alignSeries(
261
+ points: SeriesPoint[],
262
+ axis: SeriesPoint[],
263
+ ): SeriesPoint[] {
264
+ return points.slice(0, axis.length).map((point, index) => ({
265
+ x: (axis[index] as SeriesPoint).x,
266
+ y: point.y,
158
267
  }));
159
268
  }
160
269