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