@aglyn/plugins-marketing 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +56 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/announcement-bar-card.component.d.ts +20 -0
- package/src/lib/components/announcement-bar-card.component.js +292 -0
- package/src/lib/components/announcement-bar-card.component.js.map +1 -0
- package/src/lib/components/campaign-composer.d.ts +84 -0
- package/src/lib/components/campaign-composer.js +1643 -0
- package/src/lib/components/campaign-composer.js.map +1 -0
- package/src/lib/components/campaign-conversions-card.d.ts +22 -0
- package/src/lib/components/campaign-conversions-card.js +642 -0
- package/src/lib/components/campaign-conversions-card.js.map +1 -0
- package/src/lib/components/campaign-detail-card.d.ts +55 -0
- package/src/lib/components/campaign-detail-card.js +771 -0
- package/src/lib/components/campaign-detail-card.js.map +1 -0
- package/src/lib/components/campaign-edit-drawer.d.ts +60 -0
- package/src/lib/components/campaign-edit-drawer.js +252 -0
- package/src/lib/components/campaign-edit-drawer.js.map +1 -0
- package/src/lib/components/campaign-email-zones.d.ts +111 -0
- package/src/lib/components/campaign-email-zones.js +54 -0
- package/src/lib/components/campaign-email-zones.js.map +1 -0
- package/src/lib/components/campaign-glance-card.component.d.ts +18 -0
- package/src/lib/components/campaign-glance-card.component.js +151 -0
- package/src/lib/components/campaign-glance-card.component.js.map +1 -0
- package/src/lib/components/campaign-members-section.d.ts +87 -0
- package/src/lib/components/campaign-members-section.js +499 -0
- package/src/lib/components/campaign-members-section.js.map +1 -0
- package/src/lib/components/campaign-reach-sections.d.ts +137 -0
- package/src/lib/components/campaign-reach-sections.js +694 -0
- package/src/lib/components/campaign-reach-sections.js.map +1 -0
- package/src/lib/components/campaign-report-card.d.ts +91 -0
- package/src/lib/components/campaign-report-card.js +667 -0
- package/src/lib/components/campaign-report-card.js.map +1 -0
- package/src/lib/components/campaign-test-send-drawer.d.ts +45 -0
- package/src/lib/components/campaign-test-send-drawer.js +238 -0
- package/src/lib/components/campaign-test-send-drawer.js.map +1 -0
- package/src/lib/components/campaigns-card.d.ts +31 -0
- package/src/lib/components/campaigns-card.js +667 -0
- package/src/lib/components/campaigns-card.js.map +1 -0
- package/src/lib/components/conversion-attribution.component.d.ts +62 -0
- package/src/lib/components/conversion-attribution.component.js +127 -0
- package/src/lib/components/conversion-attribution.component.js.map +1 -0
- package/src/lib/components/email-compose-card.d.ts +12 -0
- package/src/lib/components/email-compose-card.js +157 -0
- package/src/lib/components/email-compose-card.js.map +1 -0
- package/src/lib/components/email-detail.d.ts +40 -0
- package/src/lib/components/email-detail.js +1180 -0
- package/src/lib/components/email-detail.js.map +1 -0
- package/src/lib/components/email-edit-drawer.d.ts +54 -0
- package/src/lib/components/email-edit-drawer.js +172 -0
- package/src/lib/components/email-edit-drawer.js.map +1 -0
- package/src/lib/components/email-messages-widget.d.ts +22 -0
- package/src/lib/components/email-messages-widget.js +49 -0
- package/src/lib/components/email-messages-widget.js.map +1 -0
- package/src/lib/components/email-recipients-card.d.ts +50 -0
- package/src/lib/components/email-recipients-card.js +338 -0
- package/src/lib/components/email-recipients-card.js.map +1 -0
- package/src/lib/components/emails-list-card.d.ts +36 -0
- package/src/lib/components/emails-list-card.js +544 -0
- package/src/lib/components/emails-list-card.js.map +1 -0
- package/src/lib/components/experiment-zones.d.ts +78 -0
- package/src/lib/components/experiment-zones.js +65 -0
- package/src/lib/components/experiment-zones.js.map +1 -0
- package/src/lib/components/host-experiments-card.component.d.ts +17 -0
- package/src/lib/components/host-experiments-card.component.js +887 -0
- package/src/lib/components/host-experiments-card.component.js.map +1 -0
- package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
- package/src/lib/components/host-marketing-summary-card.component.js +161 -0
- package/src/lib/components/host-marketing-summary-card.component.js.map +1 -0
- package/src/lib/components/host-overlays-card.component.d.ts +19 -0
- package/src/lib/components/host-overlays-card.component.js +783 -0
- package/src/lib/components/host-overlays-card.component.js.map +1 -0
- package/src/lib/components/marketing-console-page.d.ts +28 -0
- package/src/lib/components/marketing-console-page.js +166 -0
- package/src/lib/components/marketing-console-page.js.map +1 -0
- package/src/lib/components/marketing-console-sections.d.ts +34 -0
- package/src/lib/components/marketing-console-sections.js +77 -0
- package/src/lib/components/marketing-console-sections.js.map +1 -0
- package/src/lib/components/overlay-stats-row.component.d.ts +15 -0
- package/src/lib/components/overlay-stats-row.component.js +78 -0
- package/src/lib/components/overlay-stats-row.component.js.map +1 -0
- package/src/lib/components/popup-card.component.d.ts +18 -0
- package/src/lib/components/popup-card.component.js +462 -0
- package/src/lib/components/popup-card.component.js.map +1 -0
- package/src/lib/components/record-attribution-widget.d.ts +35 -0
- package/src/lib/components/record-attribution-widget.js +33 -0
- package/src/lib/components/record-attribution-widget.js.map +1 -0
- package/src/lib/components/site-runtime.d.ts +9 -0
- package/src/lib/components/site-runtime.js +1081 -0
- package/src/lib/components/site-runtime.js.map +1 -0
- package/src/lib/components/use-campaign-send-api.d.ts +35 -0
- package/src/lib/components/use-campaign-send-api.js +99 -0
- package/src/lib/components/use-campaign-send-api.js.map +1 -0
- package/src/lib/components/use-campaign-topic-options.d.ts +20 -0
- package/src/lib/components/use-campaign-topic-options.js +44 -0
- package/src/lib/components/use-campaign-topic-options.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +39 -0
- package/src/lib/components/use-emails-hub-path.js +70 -0
- package/src/lib/components/use-emails-hub-path.js.map +1 -0
- package/src/lib/components/use-marketing-hub-path.d.ts +21 -0
- package/src/lib/components/use-marketing-hub-path.js +50 -0
- package/src/lib/components/use-marketing-hub-path.js.map +1 -0
- package/src/lib/components/use-overlay-copy-editor.d.ts +79 -0
- package/src/lib/components/use-overlay-copy-editor.js +110 -0
- package/src/lib/components/use-overlay-copy-editor.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/model/campaign-membership-figures.d.ts +109 -0
- package/src/lib/model/campaign-membership-figures.js +113 -0
- package/src/lib/model/campaign-membership-figures.js.map +1 -0
- package/src/lib/model/compile-client-automations.d.ts +65 -0
- package/src/lib/model/compile-client-automations.js +95 -0
- package/src/lib/model/compile-client-automations.js.map +1 -0
- package/src/lib/model/experiments.d.ts +130 -0
- package/src/lib/model/experiments.js +186 -0
- package/src/lib/model/experiments.js.map +1 -0
- package/src/lib/model/index.d.ts +26 -0
- package/src/lib/model/index.js +26 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/marketing-record-routes.d.ts +6 -0
- package/src/lib/model/marketing-record-routes.js +46 -0
- package/src/lib/model/marketing-record-routes.js.map +1 -0
- package/src/lib/model/overlay-copy.d.ts +62 -0
- package/src/lib/model/overlay-copy.js +78 -0
- package/src/lib/model/overlay-copy.js.map +1 -0
- package/src/lib/model/overlays.d.ts +113 -0
- package/src/lib/model/overlays.js +93 -0
- package/src/lib/model/overlays.js.map +1 -0
- package/src/lib/model/site-contract.d.ts +100 -0
- package/src/lib/model/site-contract.js +23 -0
- package/src/lib/model/site-contract.js.map +1 -0
- package/src/lib/plugin.d.ts +27 -0
- package/src/lib/plugin.js +168 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/preview-props.d.ts +31 -0
- package/src/lib/preview-props.js +54 -0
- package/src/lib/preview-props.js.map +1 -0
- package/src/lib/server/campaign-manage.d.ts +62 -0
- package/src/lib/server/campaign-manage.js +829 -0
- package/src/lib/server/campaign-manage.js.map +1 -0
- package/src/lib/server/campaign-process-scheduled.d.ts +26 -0
- package/src/lib/server/campaign-process-scheduled.js +198 -0
- package/src/lib/server/campaign-process-scheduled.js.map +1 -0
- package/src/lib/server/campaign-recipients.d.ts +55 -0
- package/src/lib/server/campaign-recipients.js +219 -0
- package/src/lib/server/campaign-recipients.js.map +1 -0
- package/src/lib/server/campaign-send.d.ts +487 -0
- package/src/lib/server/campaign-send.js +3157 -0
- package/src/lib/server/campaign-send.js.map +1 -0
- package/src/lib/server/email-campaign-reach.d.ts +174 -0
- package/src/lib/server/email-campaign-reach.js +254 -0
- package/src/lib/server/email-campaign-reach.js.map +1 -0
- package/src/lib/server/email-events.d.ts +27 -0
- package/src/lib/server/email-events.js +714 -0
- package/src/lib/server/email-events.js.map +1 -0
- package/src/lib/server/get-client-automations.d.ts +60 -0
- package/src/lib/server/get-client-automations.js +87 -0
- package/src/lib/server/get-client-automations.js.map +1 -0
- package/src/lib/server/get-overlays.d.ts +28 -0
- package/src/lib/server/get-overlays.js +65 -0
- package/src/lib/server/get-overlays.js.map +1 -0
- package/src/lib/server/get-screen-experiments.d.ts +42 -0
- package/src/lib/server/get-screen-experiments.js +82 -0
- package/src/lib/server/get-screen-experiments.js.map +1 -0
- package/src/lib/server/lists-materialize.d.ts +18 -0
- package/src/lib/server/lists-materialize.js +130 -0
- package/src/lib/server/lists-materialize.js.map +1 -0
- package/src/lib/server/marketing-figures.d.ts +37 -0
- package/src/lib/server/marketing-figures.js +248 -0
- package/src/lib/server/marketing-figures.js.map +1 -0
- package/src/lib/server/site-page-enricher.d.ts +26 -0
- package/src/lib/server/site-page-enricher.js +210 -0
- package/src/lib/server/site-page-enricher.js.map +1 -0
- package/src/lib/server.d.ts +20 -0
- package/src/lib/server.js +152 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +22 -0
- package/src/lib/site.js +48 -0
- package/src/lib/site.js.map +1 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
18
|
+
import { type SendingIdentitySource } from '@aglyn/shared-util-email';
|
|
19
|
+
/**
|
|
20
|
+
* Stable doc id for a suppression entry (emails are PII — hash them).
|
|
21
|
+
*
|
|
22
|
+
* A WRITER's derivation now — `email-events.ts` files bounces and complaints
|
|
23
|
+
* under it. The send path reads through `emailSuppressionKey`, which hashes
|
|
24
|
+
* the same trimmed, lowercased form and additionally refuses to guess an id
|
|
25
|
+
* for a value that is not an address.
|
|
26
|
+
*/
|
|
27
|
+
export declare function suppressionId(email: string): string;
|
|
28
|
+
/** HMAC for unsubscribe links; env-gated on the shared secret. */
|
|
29
|
+
export declare function unsubscribeSignature(hostId: string, email: string, secret: string, campaignId?: string, topicId?: string): string;
|
|
30
|
+
/** Send failures carry the HTTP status the API route should answer. */
|
|
31
|
+
export declare class CampaignSendError extends Error {
|
|
32
|
+
readonly status: number;
|
|
33
|
+
constructor(message: string, status: number);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The platform send rate had no room for this campaign THIS HOUR (AGL-2409).
|
|
37
|
+
*
|
|
38
|
+
* A subclass rather than a status code the caller has to recognise, because
|
|
39
|
+
* the difference it carries is not "which HTTP number" — it is **retry, do not
|
|
40
|
+
* fail**. A scheduled campaign that trips the hourly ceiling must go back to
|
|
41
|
+
* `scheduled` and be picked up by the next 15-minute run; marking it `failed`,
|
|
42
|
+
* which is what every other `CampaignSendError` correctly does, would turn a
|
|
43
|
+
* ramp into a lost campaign that a merchant has to notice and re-create.
|
|
44
|
+
*
|
|
45
|
+
* Thrown ONLY before anything has been sent, so a caller that retries cannot
|
|
46
|
+
* double-send.
|
|
47
|
+
*/
|
|
48
|
+
export declare class CampaignSendDeferredError extends CampaignSendError {
|
|
49
|
+
/** When the hourly window rolls. */
|
|
50
|
+
readonly retryAtMs: number;
|
|
51
|
+
constructor(message: string,
|
|
52
|
+
/** When the hourly window rolls. */
|
|
53
|
+
retryAtMs: number);
|
|
54
|
+
}
|
|
55
|
+
export interface CampaignSendOptions {
|
|
56
|
+
hostId: string;
|
|
57
|
+
subject: string;
|
|
58
|
+
/**
|
|
59
|
+
* The MESSAGE of a plain-text email — what recipients read, and what the
|
|
60
|
+
* HTML part is synthesized from.
|
|
61
|
+
*
|
|
62
|
+
* Not read on a designed send, where the nodes are the message. The route
|
|
63
|
+
* refuses a request carrying both rather than accepting one and dropping it,
|
|
64
|
+
* which is what it used to do.
|
|
65
|
+
*/
|
|
66
|
+
body: string;
|
|
67
|
+
audience: string;
|
|
68
|
+
segmentId?: string;
|
|
69
|
+
listId?: string;
|
|
70
|
+
/**
|
|
71
|
+
* The stream this campaign belongs to, chosen in the composer.
|
|
72
|
+
*
|
|
73
|
+
* Resolved to {@link DEFAULT_CAMPAIGN_TOPIC_ID} when absent, so every send
|
|
74
|
+
* belongs to some topic: a campaign with none would mint an unsubscribe link
|
|
75
|
+
* the preference page can render but not place, offering the recipient a
|
|
76
|
+
* catalog without saying which entry the message in front of them was.
|
|
77
|
+
*/
|
|
78
|
+
topicId?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the audience and return the count WITHOUT sending anything
|
|
81
|
+
* (AGL-2178). Returns before the first write, so it mints no campaign
|
|
82
|
+
* id and touches no counter.
|
|
83
|
+
*/
|
|
84
|
+
dryRun?: boolean;
|
|
85
|
+
emails?: string[];
|
|
86
|
+
campaignId?: string;
|
|
87
|
+
experimentId?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Designed email template (AGL-349): screen id of a besigner email
|
|
90
|
+
* document, which is what makes this a DESIGNED send.
|
|
91
|
+
*
|
|
92
|
+
* The nodes produce both parts an inbox receives — the HTML and a plain-text
|
|
93
|
+
* rendering of the same design — so `body` has no job here and is not read.
|
|
94
|
+
* `plainText` below is the one thing an author may substitute.
|
|
95
|
+
*/
|
|
96
|
+
templateScreenId?: string;
|
|
97
|
+
/**
|
|
98
|
+
* The author's own PLAIN-TEXT PART for a designed email.
|
|
99
|
+
*
|
|
100
|
+
* Empty means the design generates it, which is the default and what every
|
|
101
|
+
* designed campaign did before this existed. Read only on the designed
|
|
102
|
+
* path: a plain-text campaign's text part IS its body, and a second string
|
|
103
|
+
* claiming to be the text part beside it would be the two-sources problem
|
|
104
|
+
* again under a new name.
|
|
105
|
+
*/
|
|
106
|
+
plainText?: string;
|
|
107
|
+
/**
|
|
108
|
+
* The sender's DISPLAY NAME for this campaign, overriding the org's
|
|
109
|
+
* branding default.
|
|
110
|
+
*
|
|
111
|
+
* A display name and nothing more: `applyFromName` keeps the verified
|
|
112
|
+
* address it is applied to, so this cannot move the mail onto a domain the
|
|
113
|
+
* org has not proved. The route strips control characters before it gets
|
|
114
|
+
* here, because the value is merchant-typed and lands in a header.
|
|
115
|
+
*/
|
|
116
|
+
fromName?: string;
|
|
117
|
+
/** Where replies go, when it is not the sending address. */
|
|
118
|
+
replyTo?: string;
|
|
119
|
+
/**
|
|
120
|
+
* WHICH OF THE SITE'S SENDERS THIS EMAIL LEAVES AS —
|
|
121
|
+
* `hosts/{hostId}/senders/{senderId}`.
|
|
122
|
+
*
|
|
123
|
+
* The mailbox, and only the mailbox. A sender also carries a display name
|
|
124
|
+
* and a reply address, and those stay per-send fields resolved from
|
|
125
|
+
* {@link fromName} and {@link replyTo}: the composer seeds them from the
|
|
126
|
+
* chosen sender so a merchant sees what will go out, and the send records
|
|
127
|
+
* what was submitted, which is what keeps the stored report and the message
|
|
128
|
+
* one answer rather than two.
|
|
129
|
+
*
|
|
130
|
+
* ## An id, which is not the input path that was closed
|
|
131
|
+
*
|
|
132
|
+
* `req.body.sendingIdentity` is read by nothing, and a per-send local part
|
|
133
|
+
* would reopen it: a mailbox is where a bounce returns and where a client
|
|
134
|
+
* ignoring `Reply-To:` answers, so one that exists in a single campaign's
|
|
135
|
+
* headers is an address nobody serves. A sender id names a mailbox that was
|
|
136
|
+
* configured once, validated once and stored on this site under an
|
|
137
|
+
* `org.settings` gate. The set of addresses this field can reach is the set
|
|
138
|
+
* an org admin already approved.
|
|
139
|
+
*
|
|
140
|
+
* Absent means the site's DEFAULT sender, which is the projection the host
|
|
141
|
+
* document has always carried. Naming a sender this site does not hold is
|
|
142
|
+
* REFUSED — see the resolution below — rather than falling back to it.
|
|
143
|
+
*/
|
|
144
|
+
senderId?: string;
|
|
145
|
+
/**
|
|
146
|
+
* The CAMPAIGN this send belongs to — `hosts/{hostId}/emailCampaigns/{id}`.
|
|
147
|
+
*
|
|
148
|
+
* Not the send's own id, which is what `campaignId` means here and what
|
|
149
|
+
* every delivered unsubscribe link carries as `cid`. Absent on a send
|
|
150
|
+
* composed outside a campaign, and on every send that predates containers;
|
|
151
|
+
* the campaigns list adopts those as a campaign of one at read time rather
|
|
152
|
+
* than rewriting them.
|
|
153
|
+
*/
|
|
154
|
+
emailCampaignId?: string;
|
|
155
|
+
/**
|
|
156
|
+
* The preview line inboxes show after the subject. Overrides a designed
|
|
157
|
+
* template's own, and gives a plain-text campaign one at all.
|
|
158
|
+
*/
|
|
159
|
+
preheader?: string;
|
|
160
|
+
/**
|
|
161
|
+
* What the merchant called this email, for the record rather than for the
|
|
162
|
+
* recipient.
|
|
163
|
+
*
|
|
164
|
+
* Never leaves the console — it is not the subject and reaches no header,
|
|
165
|
+
* so it is stored as typed apart from a length cap. Absent on every send
|
|
166
|
+
* composed from a campaign, where the campaign is what carries the name.
|
|
167
|
+
*/
|
|
168
|
+
displayName?: string;
|
|
169
|
+
/** Test sends (AGL-349) skip the campaign record and stats. */
|
|
170
|
+
recordCampaign?: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* SEND THIS EMAIL AGAIN, TO PEOPLE IT HAS NOT REACHED.
|
|
173
|
+
*
|
|
174
|
+
* Set with a `campaignId` naming a send that has already gone out. The send
|
|
175
|
+
* is not copied and no second document is made: this send addresses the
|
|
176
|
+
* same audience under the same id, minus everyone the earlier sends
|
|
177
|
+
* reached, and ADDS to the counters already on the record.
|
|
178
|
+
*
|
|
179
|
+
* ## Why the same document, rather than a new send
|
|
180
|
+
*
|
|
181
|
+
* Two reasons, and both are properties a copy would break.
|
|
182
|
+
*
|
|
183
|
+
* The unsubscribe link is the first. Every message this email has already
|
|
184
|
+
* delivered carries `cid={campaignId}` inside its own HMAC, and those
|
|
185
|
+
* messages sit in inboxes forever. A follow-up under a new id would be a
|
|
186
|
+
* second `cid` for one email — two opt-out scopes for one mailing, and an
|
|
187
|
+
* unsubscribe rate split across two records neither of which is the answer.
|
|
188
|
+
*
|
|
189
|
+
* The report is the second. Opens and clicks are attributed by the
|
|
190
|
+
* `campaignId` tag on the delivered message, so a copy would collect the
|
|
191
|
+
* follow-up's engagement on a document whose `sent` counts only the
|
|
192
|
+
* follow-up. Keeping one document keeps every rate over a denominator that
|
|
193
|
+
* covers the same mail the numerator does — see the additive write at the
|
|
194
|
+
* bottom of this function for the arithmetic that holds it.
|
|
195
|
+
*
|
|
196
|
+
* ## What it does NOT relax
|
|
197
|
+
*
|
|
198
|
+
* Nothing. The follow-up runs the whole of this function: the consent
|
|
199
|
+
* split, both suppression lists, the topic filter, the monthly pre-check
|
|
200
|
+
* and reservation, the platform and per-workspace hourly ceilings, and the
|
|
201
|
+
* per-message governor. Its only additions are the subtraction below and
|
|
202
|
+
* the shape of the write at the end.
|
|
203
|
+
*/
|
|
204
|
+
followUp?: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* THE NEXT BATCH OF AN EMAIL THAT IS STILL GOING OUT.
|
|
207
|
+
*
|
|
208
|
+
* Set by the scheduled-campaign processor, never by a request. An audience
|
|
209
|
+
* larger than {@link EMAIL_MAX_RECIPIENTS_PER_SEND} is delivered across
|
|
210
|
+
* several invocations; each one addresses the people the earlier ones did
|
|
211
|
+
* not, and the campaign is written back as `scheduled` until nothing is
|
|
212
|
+
* left. See the batch plan at the bottom of this function.
|
|
213
|
+
*
|
|
214
|
+
* ## Why it is not {@link followUp}
|
|
215
|
+
*
|
|
216
|
+
* They share the subtraction and the additive write, and they differ on the
|
|
217
|
+
* two things that matter. A follow-up is a MERCHANT's act on an email that
|
|
218
|
+
* has finished — it subtracts who was reached, and somebody suppressed at
|
|
219
|
+
* the time who has since been released is entitled to get it. A batch is
|
|
220
|
+
* one email still in flight — it subtracts everyone the email has SETTLED,
|
|
221
|
+
* reached or refused, because a refused address sitting at the head of a
|
|
222
|
+
* stable order would consume a slot in every remaining batch and, in
|
|
223
|
+
* enough numbers, stop the campaign dead having mailed nobody.
|
|
224
|
+
*
|
|
225
|
+
* They also record differently. A follow-up measures a NEW population, so
|
|
226
|
+
* the audience size and the consent split add. A batch measures a slice of
|
|
227
|
+
* the population the first batch already measured, so those figures are
|
|
228
|
+
* left exactly as the first batch wrote them.
|
|
229
|
+
*/
|
|
230
|
+
continuation?: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* ONE address the caller has been proved to be entitled to proof to, for
|
|
233
|
+
* the composer's test send. Exempts exactly this address from the
|
|
234
|
+
* marketing-consent rule.
|
|
235
|
+
*
|
|
236
|
+
* The route decides entitlement and this file decides what the exemption
|
|
237
|
+
* can do. See {@link eligibleProofAddress} for the first half and the
|
|
238
|
+
* carve-out at the consent join for the second — including the two
|
|
239
|
+
* properties that keep the exemption one address wide however the caller
|
|
240
|
+
* arrived at it.
|
|
241
|
+
*/
|
|
242
|
+
proofFor?: string;
|
|
243
|
+
/**
|
|
244
|
+
* Test send only: render every message as if it were addressed to this
|
|
245
|
+
* person, while DELIVERING to the recipient resolved in the ordinary way.
|
|
246
|
+
*
|
|
247
|
+
* What a merge tag resolves to is the whole question a proof answers. A
|
|
248
|
+
* test rendered against the tester shows `{{firstName|there}}` falling back
|
|
249
|
+
* for an audience whose contacts all have names, which reports the merge
|
|
250
|
+
* tags as broken when they work — and, more expensively, hides the reverse.
|
|
251
|
+
*
|
|
252
|
+
* It changes the RENDER and nothing else. The unsubscribe link, the
|
|
253
|
+
* `List-Unsubscribe` header and the suppression key are all minted from the
|
|
254
|
+
* delivery address, deliberately: a proof carrying the persona's opt-out
|
|
255
|
+
* link would let the person testing the email unsubscribe a real contact by
|
|
256
|
+
* clicking a link in their own inbox.
|
|
257
|
+
*/
|
|
258
|
+
proofPersona?: {
|
|
259
|
+
email: string;
|
|
260
|
+
name?: string;
|
|
261
|
+
};
|
|
262
|
+
/** Recorded as `sentBy`; the scheduler passes the scheduling user. */
|
|
263
|
+
senderUid: string;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Campaign delivery core (AGL-161, extracted for AGL-272): resolves the
|
|
267
|
+
* audience server-side, drops suppressed addresses, enforces the plan's
|
|
268
|
+
* monthly send cap, personalizes merge tags per recipient, and delivers
|
|
269
|
+
* through Resend with a signed unsubscribe link. Shared by the
|
|
270
|
+
* authenticated send route and the scheduled-campaign processor. The
|
|
271
|
+
* caller owns authorization.
|
|
272
|
+
*/
|
|
273
|
+
export interface CampaignSendResult {
|
|
274
|
+
campaignId: string;
|
|
275
|
+
/** Addresses this send ADDRESSED — the audience after the per-send cap. */
|
|
276
|
+
recipients: number;
|
|
277
|
+
sent: number;
|
|
278
|
+
/**
|
|
279
|
+
* The whole audience, deduplicated and validated, BEFORE the per-send cap.
|
|
280
|
+
*
|
|
281
|
+
* Reported separately from `recipients` because the two differ whenever an
|
|
282
|
+
* audience is larger than one send may carry, and a merchant who is only
|
|
283
|
+
* shown the smaller number has no way to learn that the rest were never
|
|
284
|
+
* mailed. `recipients` of 500 against an `audienceSize` of 3,000 is the
|
|
285
|
+
* whole point of the field.
|
|
286
|
+
*
|
|
287
|
+
* Named for the SIZE because `audience` on the options and on the stored
|
|
288
|
+
* campaign is the audience KIND — `'leads'`, `'list'` — and one word
|
|
289
|
+
* meaning both a name and a count on the same send path is how the two get
|
|
290
|
+
* read into each other.
|
|
291
|
+
*/
|
|
292
|
+
audienceSize: number;
|
|
293
|
+
/**
|
|
294
|
+
* `audienceSize` is a FLOOR, not a total: the resolution stopped at its
|
|
295
|
+
* read ceiling with documents still unread. Absent means it is exact.
|
|
296
|
+
*/
|
|
297
|
+
audienceTruncated?: boolean;
|
|
298
|
+
/**
|
|
299
|
+
* Dry run only (AGL-2178): recipients that will actually be mailed —
|
|
300
|
+
* after the consent join, the per-send cap and both suppression lists.
|
|
301
|
+
*/
|
|
302
|
+
sendable?: number;
|
|
303
|
+
/** Dry run only: of `audienceSize`, how many carry a recorded consent basis. */
|
|
304
|
+
consented?: number;
|
|
305
|
+
/**
|
|
306
|
+
* Dry run only: of `consented`, how many hold a basis an OPERATOR asserted
|
|
307
|
+
* on their behalf rather than one they gave — a backfill over seed data,
|
|
308
|
+
* say. A subset of `consented` and not a fourth population.
|
|
309
|
+
*/
|
|
310
|
+
consentedByOperator?: number;
|
|
311
|
+
/**
|
|
312
|
+
* Dry run only: of `audienceSize`, how many are reachable only because
|
|
313
|
+
* consent enforcement is not retroactive. This is the population a strict
|
|
314
|
+
* policy would remove.
|
|
315
|
+
*/
|
|
316
|
+
grandfathered?: number;
|
|
317
|
+
/** Dry run only: how many of `audienceSize` the consent rule refused. */
|
|
318
|
+
consentWithheld?: number;
|
|
319
|
+
/**
|
|
320
|
+
* Dry run only: how many of `recipients` are suppressed or off-topic.
|
|
321
|
+
*
|
|
322
|
+
* Not netted with {@link cadenceHeld}: leaving this site and asking it for
|
|
323
|
+
* less mail are different statements, and only one of them is a list a
|
|
324
|
+
* merchant has to rebuild.
|
|
325
|
+
*/
|
|
326
|
+
suppressed?: number;
|
|
327
|
+
/**
|
|
328
|
+
* Dry run only: how many of `recipients` asked this site for mail less
|
|
329
|
+
* often than this send would arrive.
|
|
330
|
+
*
|
|
331
|
+
* They stay on every audience they were on and are mailed by the next
|
|
332
|
+
* campaign that falls outside their interval — the filter refuses a SEND,
|
|
333
|
+
* never a person.
|
|
334
|
+
*/
|
|
335
|
+
cadenceHeld?: number;
|
|
336
|
+
/** Dry run only: which sending identity this campaign would leave on. */
|
|
337
|
+
identity?: string;
|
|
338
|
+
/**
|
|
339
|
+
* Dry run only: which KIND of identity this campaign would leave on.
|
|
340
|
+
*
|
|
341
|
+
* The union from the resolver rather than a local copy of it, so a new arm
|
|
342
|
+
* cannot be added there and silently narrowed here. `'shared'` never reaches
|
|
343
|
+
* a real campaign — the resolution above declares `purpose: 'marketing'`, and
|
|
344
|
+
* a pooled identity refuses before this is assembled — but the type says what
|
|
345
|
+
* the resolver can return, not what this path expects to see.
|
|
346
|
+
*/
|
|
347
|
+
identitySource?: SendingIdentitySource | null;
|
|
348
|
+
dryRun?: boolean;
|
|
349
|
+
/** Recipients the hourly governor refused mid-batch (AGL-2409). */
|
|
350
|
+
deferred?: number;
|
|
351
|
+
/**
|
|
352
|
+
* Follow-up only: people in the audience this email had already reached, and
|
|
353
|
+
* which this send therefore did not address.
|
|
354
|
+
*
|
|
355
|
+
* Reported rather than netted away, because it is the number that explains
|
|
356
|
+
* the other ones. A follow-up over a 3,000-person list that addresses 40
|
|
357
|
+
* people has not failed — 2,960 of them already have the email — and
|
|
358
|
+
* without this figure the merchant cannot tell that from a broken audience.
|
|
359
|
+
*/
|
|
360
|
+
alreadyReached?: number;
|
|
361
|
+
/** Whether this send added to an existing email rather than starting one. */
|
|
362
|
+
followUp?: boolean;
|
|
363
|
+
/**
|
|
364
|
+
* People this email has resolved and not yet addressed.
|
|
365
|
+
*
|
|
366
|
+
* Non-zero on a send that is still going out, and the figure that makes
|
|
367
|
+
* "reached 500 of 3,000" a sentence rather than a truncation. Zero when the
|
|
368
|
+
* email is finished.
|
|
369
|
+
*/
|
|
370
|
+
remaining?: number;
|
|
371
|
+
/** True when another batch of this email will run on its own. */
|
|
372
|
+
resuming?: boolean;
|
|
373
|
+
/** When the next batch may go, ms. Present only while {@link resuming}. */
|
|
374
|
+
nextAtMs?: number;
|
|
375
|
+
/** Batches this email has run, including this one. */
|
|
376
|
+
batch?: number;
|
|
377
|
+
}
|
|
378
|
+
export declare function performCampaignSend(options: CampaignSendOptions): Promise<CampaignSendResult>;
|
|
379
|
+
/**
|
|
380
|
+
* The stored configuration of a send, as the options that would mail it.
|
|
381
|
+
*
|
|
382
|
+
* Every field comes off the RECORD and none of it off the request — the
|
|
383
|
+
* caller names a site and an email id and nothing else. That is not tidiness:
|
|
384
|
+
* `campaignId` addresses an existing document, so a caller who could also
|
|
385
|
+
* supply the body and the audience could put arbitrary copy on somebody
|
|
386
|
+
* else's send id, keep its `cid` and its report, and mail it.
|
|
387
|
+
*
|
|
388
|
+
* Both callers rest on that. A follow-up mails the email that is already
|
|
389
|
+
* there; `sendNow` mails a draft or a scheduled email ahead of its time, and
|
|
390
|
+
* the copy it delivers has to be the copy that was composed and previewed
|
|
391
|
+
* rather than whatever a request body happens to carry.
|
|
392
|
+
*
|
|
393
|
+
* Refuses a send carrying neither a template nor a body, which has no message
|
|
394
|
+
* whichever caller asked.
|
|
395
|
+
*/
|
|
396
|
+
/** One address a test send is allowed to be delivered to. */
|
|
397
|
+
export interface ProofRecipient {
|
|
398
|
+
email: string;
|
|
399
|
+
/** A person's name where the membership record carries one. */
|
|
400
|
+
label: string;
|
|
401
|
+
/** True for the caller's own account address. */
|
|
402
|
+
self: boolean;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* THE ADDRESSES A TEST SEND MAY BE DELIVERED TO.
|
|
406
|
+
*
|
|
407
|
+
* The caller's own account address, plus every account holder on the org that
|
|
408
|
+
* owns this site. Exported because the composer has to OFFER this set — a
|
|
409
|
+
* free-text box beside a rule enforced on the server is a box whose every
|
|
410
|
+
* wrong answer is a refusal the person could not have predicted.
|
|
411
|
+
*
|
|
412
|
+
* ## Why membership, and not "any address the merchant types"
|
|
413
|
+
*
|
|
414
|
+
* A test send is exempt from the marketing-consent rule (see the proof
|
|
415
|
+
* carve-out in `performCampaignSend`). An exemption that could be pointed at
|
|
416
|
+
* any address would not be a test-send feature, it would be a way to mail
|
|
417
|
+
* anybody without consent and call it a test. Membership is the narrowest
|
|
418
|
+
* boundary that still answers what the button is for: proofing a draft to the
|
|
419
|
+
* people who work on it.
|
|
420
|
+
*
|
|
421
|
+
* ## What is NOT here
|
|
422
|
+
*
|
|
423
|
+
* `siteMembers`, `leads` and contacts. They are the tenant's audience, and
|
|
424
|
+
* the audience is precisely the population the consent rule protects. A
|
|
425
|
+
* contact can be chosen as the PERSONA a proof renders as — see
|
|
426
|
+
* {@link resolveProofPersona} — which reaches nobody.
|
|
427
|
+
*
|
|
428
|
+
* ## What this does not relax
|
|
429
|
+
*
|
|
430
|
+
* Everything else. The send still runs both suppression lists, so an address
|
|
431
|
+
* that bounced or complained is refused however senior its owner; and a
|
|
432
|
+
* stored `declined` on the address still refuses, because a refusal is the
|
|
433
|
+
* one thing no policy may mail.
|
|
434
|
+
*/
|
|
435
|
+
export declare function proofRecipientsForHost(options: {
|
|
436
|
+
hostId: string;
|
|
437
|
+
callerEmail: string;
|
|
438
|
+
}): Promise<ProofRecipient[]>;
|
|
439
|
+
/** One person a proof can be rendered as. Reaches nobody. */
|
|
440
|
+
export interface ProofPersona {
|
|
441
|
+
email: string;
|
|
442
|
+
name: string;
|
|
443
|
+
/** Which audience the record came from, so the drawer can say. */
|
|
444
|
+
source: 'lead' | 'member' | 'contact';
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* A SAMPLE of the people this site's mail is addressed to, for the picker.
|
|
448
|
+
*
|
|
449
|
+
* A sample and not a search: the question the drawer asks is "show me this
|
|
450
|
+
* email as somebody real", and twenty names from each source answers it for
|
|
451
|
+
* the cost of three small reads. A contact picker with a query behind it is a
|
|
452
|
+
* different feature, and it would put a text input in front of the org's
|
|
453
|
+
* whole contact list on a surface whose job is to prove one email.
|
|
454
|
+
*
|
|
455
|
+
* Ordered by document id in every source, because a `limit` with no `orderBy`
|
|
456
|
+
* is a random sample in doc-id order that a client `sort` then makes LOOK
|
|
457
|
+
* newest-first. Naming the order keeps the picker stable between openings —
|
|
458
|
+
* and on the org path it is also the order the automatic index for the scope
|
|
459
|
+
* filter can actually serve.
|
|
460
|
+
*/
|
|
461
|
+
export declare function proofPersonasForHost(hostId: string): Promise<ProofPersona[]>;
|
|
462
|
+
/**
|
|
463
|
+
* Campaign API (AGL-161/272): `action` picks the operation.
|
|
464
|
+
*
|
|
465
|
+
* Every one of them requires a site admin or editor.
|
|
466
|
+
*
|
|
467
|
+
* ## The ones that mail something
|
|
468
|
+
*
|
|
469
|
+
* `send` (the default) delivers copy carried in the request. `sendNow` mails
|
|
470
|
+
* a draft or a scheduled email ahead of its time, and `followUp` mails an
|
|
471
|
+
* already-sent one to the people it has not reached; both of those take every
|
|
472
|
+
* field off the RECORD rather than the request. `test` delivers to the caller
|
|
473
|
+
* alone and records nothing.
|
|
474
|
+
*
|
|
475
|
+
* ## The ones that only write
|
|
476
|
+
*
|
|
477
|
+
* `draft` stores an email that has not been sent, `schedule` stores one with
|
|
478
|
+
* a `sendAtMs` for the processor to deliver, `update` corrects the merchant's
|
|
479
|
+
* own name for an email at any point in its life, and `cancel` withdraws a
|
|
480
|
+
* scheduled one. None of them reserves allowance or moves a meter.
|
|
481
|
+
*
|
|
482
|
+
* ## The ones that answer a question
|
|
483
|
+
*
|
|
484
|
+
* `preview` resolves the audience and reports the counts, `renderPreview`
|
|
485
|
+
* renders the composed message. Neither writes.
|
|
486
|
+
*/
|
|
487
|
+
export declare const campaignSendHandler: PluginApiHandler;
|