@aglyn/plugins-marketing 1.0.0-beta.172 → 1.0.0-beta.174
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 +2 -1
- package/package.json +15 -15
- package/src/lib/components/campaign-detail-card.d.ts +12 -1
- package/src/lib/components/campaign-detail-card.js +146 -35
- package/src/lib/components/campaign-detail-card.js.map +1 -1
- package/src/lib/components/campaign-edit-drawer.d.ts +13 -0
- package/src/lib/components/campaign-edit-drawer.js +56 -4
- package/src/lib/components/campaign-edit-drawer.js.map +1 -1
- package/src/lib/components/campaign-glance-card.component.js +10 -2
- package/src/lib/components/campaign-glance-card.component.js.map +1 -1
- package/src/lib/components/campaign-members-section.d.ts +6 -0
- package/src/lib/components/campaign-members-section.js +2 -2
- package/src/lib/components/campaign-members-section.js.map +1 -1
- package/src/lib/components/campaign-queries.d.ts +58 -0
- package/src/lib/components/campaign-queries.js +72 -0
- package/src/lib/components/campaign-queries.js.map +1 -0
- package/src/lib/components/campaign-reach-sections.d.ts +21 -7
- package/src/lib/components/campaign-reach-sections.js +20 -17
- package/src/lib/components/campaign-reach-sections.js.map +1 -1
- package/src/lib/components/campaign-report-card.d.ts +8 -1
- package/src/lib/components/campaign-report-card.js +33 -16
- package/src/lib/components/campaign-report-card.js.map +1 -1
- package/src/lib/components/campaigns-card.d.ts +10 -1
- package/src/lib/components/campaigns-card.js +102 -19
- package/src/lib/components/campaigns-card.js.map +1 -1
- package/src/lib/components/email-compose-card.d.ts +3 -2
- package/src/lib/components/email-compose-card.js +18 -7
- package/src/lib/components/email-compose-card.js.map +1 -1
- package/src/lib/components/email-detail.d.ts +11 -2
- package/src/lib/components/email-detail.js +50 -23
- package/src/lib/components/email-detail.js.map +1 -1
- package/src/lib/components/emails-list-card.d.ts +10 -1
- package/src/lib/components/emails-list-card.js +129 -23
- package/src/lib/components/emails-list-card.js.map +1 -1
- package/src/lib/components/host-marketing-summary-card.component.js +9 -1
- package/src/lib/components/host-marketing-summary-card.component.js.map +1 -1
- package/src/lib/components/marketing-console-page.js +81 -4
- package/src/lib/components/marketing-console-page.js.map +1 -1
- package/src/lib/components/marketing-console-sections.d.ts +16 -1
- package/src/lib/components/marketing-console-sections.js +25 -2
- package/src/lib/components/marketing-console-sections.js.map +1 -1
- package/src/lib/components/marketing-org-mount.d.ts +71 -0
- package/src/lib/components/marketing-org-mount.js +84 -0
- package/src/lib/components/marketing-org-mount.js.map +1 -0
- package/src/lib/components/use-campaign-send-api.d.ts +2 -2
- package/src/lib/components/use-campaign-send-api.js +22 -6
- package/src/lib/components/use-campaign-send-api.js.map +1 -1
- package/src/lib/components/use-emails-hub-path.d.ts +13 -3
- package/src/lib/components/use-emails-hub-path.js +25 -4
- package/src/lib/components/use-emails-hub-path.js.map +1 -1
- package/src/lib/components/use-marketing-hub-path.d.ts +19 -16
- package/src/lib/components/use-marketing-hub-path.js +6 -32
- package/src/lib/components/use-marketing-hub-path.js.map +1 -1
- package/src/lib/model/marketing-record-routes.js +11 -4
- package/src/lib/model/marketing-record-routes.js.map +1 -1
- package/src/lib/plugin.js +31 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/campaign-manage.d.ts +31 -2
- package/src/lib/server/campaign-manage.js +365 -137
- package/src/lib/server/campaign-manage.js.map +1 -1
- package/src/lib/server/campaign-org-refs.d.ts +45 -0
- package/src/lib/server/campaign-org-refs.js +83 -0
- package/src/lib/server/campaign-org-refs.js.map +1 -0
- package/src/lib/server/campaign-process-scheduled.d.ts +32 -2
- package/src/lib/server/campaign-process-scheduled.js +79 -9
- package/src/lib/server/campaign-process-scheduled.js.map +1 -1
- package/src/lib/server/campaign-recipients.js +18 -4
- package/src/lib/server/campaign-recipients.js.map +1 -1
- package/src/lib/server/campaign-send.js +139 -55
- package/src/lib/server/campaign-send.js.map +1 -1
- package/src/lib/server/email-campaign-reach.d.ts +6 -5
- package/src/lib/server/email-campaign-reach.js +14 -14
- package/src/lib/server/email-campaign-reach.js.map +1 -1
- package/src/lib/server/email-events.js +48 -17
- package/src/lib/server/email-events.js.map +1 -1
- package/src/lib/server/marketing-figures.js +12 -1
- package/src/lib/server/marketing-figures.js.map +1 -1
- package/src/lib/server.js +7 -3
- package/src/lib/server.js.map +1 -1
|
@@ -17,6 +17,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
17
17
|
*/ import { checkQuota, contactMatchesSegment, isEmailTopicId, DEFAULT_CAMPAIGN_TOPIC_ID, readMarketingBasis, resolveMarketingConsentPolicy, splitByMarketingConsent, createResourceUid, decodeStoredNodes, emailStarterSendBlock, resolveBrandingProfile, visibleToHost } from "@aglyn/aglyn/server";
|
|
18
18
|
import { renderRecipientEmail } from "@aglyn/aglyn/app-utils/recipient-email-render";
|
|
19
19
|
import { assignExperimentVariant } from "../model/index.js";
|
|
20
|
+
import { campaignPlacedOnHost } from "@aglyn/shared-ui-email-campaigns/model";
|
|
20
21
|
import { readPluginRecordCard } from "@aglyn/aglyn/plugin-manager/plugin-record-cards";
|
|
21
22
|
import { hostPublicOrigin } from "@aglyn/aglyn/server";
|
|
22
23
|
import { consentGroupForSite, orgDataCollectionForHost, orgDataQueryForHost, filterSendableForHost, filterTopicSendable, firebaseAdmin, getOrgForHost, meterHostEmail, claimOrgEmailSendBudget, orgCampaignEmailSendsForMonth, readEmailSendRateConfig, readEmailSendRateWindow, reconcileCampaignSendReservation, reserveCampaignEmailSends, resolveHostSendingIdentity } from "@aglyn/tenant-data-admin";
|
|
@@ -48,6 +49,7 @@ import { filterCadenceSendable, recordMarketingSends } from "@aglyn/tenant-data-
|
|
|
48
49
|
* that decides who has already had this email — is the one that must come
|
|
49
50
|
* from the real module.
|
|
50
51
|
*/ import { CAMPAIGN_REACH_CEILING, campaignReachCovers, campaignSettledSize, partitionByCampaignReach, readCampaignReach, readCampaignSettled, recordCampaignReach, recordCampaignSkipped } from "./email-campaign-reach.js";
|
|
52
|
+
import { campaignSendSiteStamp, orgCampaignSends, orgEmailCampaigns, sendIsOnHost } from "./campaign-org-refs.js";
|
|
51
53
|
/*
|
|
52
54
|
* The LEAF module for the reputation controls too, and for the same reason
|
|
53
55
|
* as the three above it: every spec that reaches this file mocks the
|
|
@@ -69,6 +71,7 @@ import { EMAIL_MAX_AUDIENCE_PER_SEND, EMAIL_MAX_RECIPIENTS_PER_SEND, campaignBat
|
|
|
69
71
|
* nobody could check against the other two.
|
|
70
72
|
*/ const MAX_RECIPIENTS_PER_SEND = EMAIL_MAX_RECIPIENTS_PER_SEND;
|
|
71
73
|
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
74
|
+
/** What Resend accepts as a tag value; anything else fails the message. */ const RESEND_TAG_VALUE = /^[A-Za-z0-9_-]{1,256}$/;
|
|
72
75
|
/** How many audience documents one Firestore round trip fetches. */ const AUDIENCE_PAGE_SIZE = 500;
|
|
73
76
|
/**
|
|
74
77
|
* The read budget for resolving one audience — a ceiling on the SCAN, not a
|
|
@@ -314,8 +317,10 @@ export async function performCampaignSend(options) {
|
|
|
314
317
|
// `hostId` is deliberately NOT guarded here, and that is measured rather
|
|
315
318
|
// than overlooked: both callers prove it first — the handler resolves the
|
|
316
319
|
// host document and checks the caller's role on it before calling in, and
|
|
317
|
-
// the scheduled processor passes
|
|
318
|
-
//
|
|
320
|
+
// the scheduled processor passes the site a send records only after
|
|
321
|
+
// `scheduledSendSite` has refused an empty or path-shaped one. The host
|
|
322
|
+
// read below then refuses a site that does not exist. A guard here could
|
|
323
|
+
// not fail today. A third caller would need to earn that.
|
|
319
324
|
for (const [name, value] of [
|
|
320
325
|
[
|
|
321
326
|
'campaignId',
|
|
@@ -372,6 +377,24 @@ export async function performCampaignSend(options) {
|
|
|
372
377
|
if (!hostSnapshot.exists) {
|
|
373
378
|
throw new CampaignSendError('Unknown site', 404);
|
|
374
379
|
}
|
|
380
|
+
/*
|
|
381
|
+
* THE ORGANIZATION, resolved before anything else is read.
|
|
382
|
+
*
|
|
383
|
+
* Every send is recorded under it (`orgs/{orgId}/campaigns`), so no branch
|
|
384
|
+
* below — the follow-up's checks, the batch's, the final write — can
|
|
385
|
+
* address the send without it. The consent policy, the plan cap, the
|
|
386
|
+
* reputation ramp and the branding further down read the same document,
|
|
387
|
+
* so this is the one read of it.
|
|
388
|
+
*
|
|
389
|
+
* A site with no organization cannot send: there is nowhere to record the
|
|
390
|
+
* send and no plan to charge it to. `getOrgForHost` failing is treated the
|
|
391
|
+
* same way — refused, with nothing written.
|
|
392
|
+
*/ const orgForHost = await getOrgForHost(hostId).catch(()=>null);
|
|
393
|
+
const orgId = String((_ref = orgForHost == null ? void 0 : orgForHost.orgId) != null ? _ref : '');
|
|
394
|
+
if (!orgId) {
|
|
395
|
+
throw new CampaignSendError('This site is not part of an organization, so it cannot send campaigns.', 409);
|
|
396
|
+
}
|
|
397
|
+
const sends = orgCampaignSends(firestore, orgId);
|
|
375
398
|
/*==========================================
|
|
376
399
|
* THE FOLLOW-UP'S ADMISSION CHECKS.
|
|
377
400
|
*
|
|
@@ -399,8 +422,8 @@ export async function performCampaignSend(options) {
|
|
|
399
422
|
if (!options.campaignId) {
|
|
400
423
|
throw new CampaignSendError('A batch has to name the email it is continuing', 400);
|
|
401
424
|
}
|
|
402
|
-
const sendSnapshot = await
|
|
403
|
-
if (!sendSnapshot.exists) {
|
|
425
|
+
const sendSnapshot = await sends.doc(options.campaignId).get();
|
|
426
|
+
if (!sendSnapshot.exists || !sendIsOnHost(sendSnapshot, hostId)) {
|
|
404
427
|
throw new CampaignSendError('Unknown email', 404);
|
|
405
428
|
}
|
|
406
429
|
/*
|
|
@@ -417,7 +440,7 @@ export async function performCampaignSend(options) {
|
|
|
417
440
|
* email has already mailed is the only thing standing between a resumed
|
|
418
441
|
* send and a second copy in somebody's inbox, and a batch that cannot
|
|
419
442
|
* read it must not run.
|
|
420
|
-
*/ const settled = await readCampaignSettled(
|
|
443
|
+
*/ const settled = await readCampaignSettled(sends.doc(options.campaignId));
|
|
421
444
|
const sentSoFar = Number((_ref1 = (_sendSnapshot_get1 = sendSnapshot.get('stats')) == null ? void 0 : _sendSnapshot_get1.sent) != null ? _ref1 : 0);
|
|
422
445
|
if (!campaignReachCovers(settled.reached, sentSoFar)) {
|
|
423
446
|
throw new CampaignSendError('This email does not have a complete record of who it reached, so ' + 'the rest of it cannot be sent without risking a second copy for ' + 'somebody who already has it.', 409);
|
|
@@ -446,7 +469,7 @@ export async function performCampaignSend(options) {
|
|
|
446
469
|
*/ const finishContinuation = async ()=>{
|
|
447
470
|
var _options_campaignId;
|
|
448
471
|
const sendId = String((_options_campaignId = options.campaignId) != null ? _options_campaignId : '');
|
|
449
|
-
await
|
|
472
|
+
await sends.doc(sendId).set(_extends({}, campaignSendSiteStamp(hostId), {
|
|
450
473
|
status: 'sent',
|
|
451
474
|
resume: {
|
|
452
475
|
remaining: 0,
|
|
@@ -454,7 +477,7 @@ export async function performCampaignSend(options) {
|
|
|
454
477
|
nextAtMs: 0
|
|
455
478
|
},
|
|
456
479
|
lastSentAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
|
|
457
|
-
}, {
|
|
480
|
+
}), {
|
|
458
481
|
merge: true
|
|
459
482
|
});
|
|
460
483
|
return {
|
|
@@ -473,8 +496,8 @@ export async function performCampaignSend(options) {
|
|
|
473
496
|
if (!options.campaignId) {
|
|
474
497
|
throw new CampaignSendError('A follow-up has to name the email it is adding to', 400);
|
|
475
498
|
}
|
|
476
|
-
const sendSnapshot = await
|
|
477
|
-
if (!sendSnapshot.exists) {
|
|
499
|
+
const sendSnapshot = await sends.doc(options.campaignId).get();
|
|
500
|
+
if (!sendSnapshot.exists || !sendIsOnHost(sendSnapshot, hostId)) {
|
|
478
501
|
throw new CampaignSendError('Unknown email', 404);
|
|
479
502
|
}
|
|
480
503
|
/*
|
|
@@ -489,7 +512,7 @@ export async function performCampaignSend(options) {
|
|
|
489
512
|
* "nobody", and that throw is deliberately not caught: the whole feature
|
|
490
513
|
* rests on being able to name who already has this email, and a send that
|
|
491
514
|
* cannot is a send that must not happen.
|
|
492
|
-
*/ const keys = await readCampaignReach(
|
|
515
|
+
*/ const keys = await readCampaignReach(sends.doc(options.campaignId));
|
|
493
516
|
const sentSoFar = Number((_ref3 = (_sendSnapshot_get3 = sendSnapshot.get('stats')) == null ? void 0 : _sendSnapshot_get3.sent) != null ? _ref3 : 0);
|
|
494
517
|
if (!campaignReachCovers(keys, sentSoFar)) {
|
|
495
518
|
throw new CampaignSendError('This email does not have a complete record of who it reached, so ' + 'it cannot be sent to more people without risking a second copy ' + 'for somebody who already has it. Compose a new email to the ' + 'people you want to add.', 409);
|
|
@@ -758,12 +781,9 @@ export async function performCampaignSend(options) {
|
|
|
758
781
|
* decision informed — it rides the same readout as `audienceSize`, so a
|
|
759
782
|
* merchant sees which population is which before sending.
|
|
760
783
|
*
|
|
761
|
-
* The
|
|
762
|
-
*
|
|
763
|
-
|
|
764
|
-
*/ const orgForHost = await getOrgForHost(hostId).catch(()=>null);
|
|
765
|
-
const orgId = String((_ref = orgForHost == null ? void 0 : orgForHost.orgId) != null ? _ref : '');
|
|
766
|
-
/*
|
|
784
|
+
* The policy lives on the org document resolved at the top of this
|
|
785
|
+
* function; the quota lines further down reuse the same read.
|
|
786
|
+
*/ /*
|
|
767
787
|
* THE SENDING IDENTITY, and the refusal when it is not usable.
|
|
768
788
|
*
|
|
769
789
|
* Resolved ABOVE the dry run on purpose. `preview` is where a merchant finds
|
|
@@ -1603,6 +1623,18 @@ export async function performCampaignSend(options) {
|
|
|
1603
1623
|
name: 'campaignId',
|
|
1604
1624
|
value: campaignId
|
|
1605
1625
|
},
|
|
1626
|
+
/*
|
|
1627
|
+
* The org the send is recorded under, so the webhook finds it
|
|
1628
|
+
* without a site lookup. Only a value Resend accepts is tagged —
|
|
1629
|
+
* ASCII letters, digits, `_` and `-`, which every generated org id
|
|
1630
|
+
* is — because a rejected tag fails the whole message; the webhook
|
|
1631
|
+
* resolves the org from the site when the tag is missing.
|
|
1632
|
+
*/ ...RESEND_TAG_VALUE.test(orgId) ? [
|
|
1633
|
+
{
|
|
1634
|
+
name: 'orgId',
|
|
1635
|
+
value: orgId
|
|
1636
|
+
}
|
|
1637
|
+
] : [],
|
|
1606
1638
|
...experiment ? [
|
|
1607
1639
|
{
|
|
1608
1640
|
name: 'experimentId',
|
|
@@ -1716,7 +1748,7 @@ export async function performCampaignSend(options) {
|
|
|
1716
1748
|
* has to. The other order would leave the record short and refuse the
|
|
1717
1749
|
* follow-up, which is also safe but loses the feature over a transient.
|
|
1718
1750
|
*/ if (options.recordCampaign !== false) {
|
|
1719
|
-
await recordCampaignReach(
|
|
1751
|
+
await recordCampaignReach(sends.doc(campaignId), reached);
|
|
1720
1752
|
/*
|
|
1721
1753
|
* And who it decided NOT to mail, under a field of its own.
|
|
1722
1754
|
*
|
|
@@ -1725,7 +1757,7 @@ export async function performCampaignSend(options) {
|
|
|
1725
1757
|
* anybody asks for one, and the batch that asks is the NEXT invocation of
|
|
1726
1758
|
* this function, which will have no way to know that these five hundred
|
|
1727
1759
|
* addresses were already considered.
|
|
1728
|
-
*/ await recordCampaignSkipped(
|
|
1760
|
+
*/ await recordCampaignSkipped(sends.doc(campaignId), settledOut);
|
|
1729
1761
|
}
|
|
1730
1762
|
// Sends are the email variant's exposures (AGL-255).
|
|
1731
1763
|
if (experiment && experiment.status === 'running') {
|
|
@@ -1822,7 +1854,7 @@ export async function performCampaignSend(options) {
|
|
|
1822
1854
|
* stand, which is the same "recorded, not recomputed" rule the populations
|
|
1823
1855
|
* below are stored under: they are true of the send that happened.
|
|
1824
1856
|
*/ const measuresTheAudience = !options.continuation;
|
|
1825
|
-
await
|
|
1857
|
+
await sends.doc(campaignId).set(_extends({}, campaignSendSiteStamp(hostId), {
|
|
1826
1858
|
subject,
|
|
1827
1859
|
body,
|
|
1828
1860
|
audience
|
|
@@ -2490,7 +2522,7 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2490
2522
|
error: 'Unauthenticated'
|
|
2491
2523
|
});
|
|
2492
2524
|
try {
|
|
2493
|
-
var _hostSnapshot_get, _ref11, _ref12, _ref13, _ref14;
|
|
2525
|
+
var _hostSnapshot_get, _ref11, _ref12, _ref13, _ref14, _ref15;
|
|
2494
2526
|
var _req_body15, _req_body16, _req_body17, _req_body18, _req_body19;
|
|
2495
2527
|
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
2496
2528
|
/*
|
|
@@ -2538,6 +2570,45 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2538
2570
|
error: 'Not a site admin or editor'
|
|
2539
2571
|
});
|
|
2540
2572
|
}
|
|
2573
|
+
/*==========================================
|
|
2574
|
+
* THE ORGANIZATION'S SENDS, AND WHICH OF THEM THIS SITE MAY TOUCH.
|
|
2575
|
+
*
|
|
2576
|
+
* Sends and campaigns are recorded under the org, so every branch below
|
|
2577
|
+
* that reads or writes one goes through `orgSends`. It is null for a site
|
|
2578
|
+
* with no organization, and each such branch refuses rather than
|
|
2579
|
+
* guessing where to write.
|
|
2580
|
+
*
|
|
2581
|
+
* The role above is a role on THIS site, while the collection holds every
|
|
2582
|
+
* site's mail. So a send addressed by id is acted on only when it is sent
|
|
2583
|
+
* as this site (`sendIsOnHost`); a sibling site's send answers exactly as
|
|
2584
|
+
* a send that does not exist, which is what it is from here.
|
|
2585
|
+
*=========================================*/ const orgForSite = await getOrgForHost(hostId).catch(()=>null);
|
|
2586
|
+
const siteOrgId = String((_ref11 = orgForSite == null ? void 0 : orgForSite.orgId) != null ? _ref11 : '');
|
|
2587
|
+
const orgSends = siteOrgId ? orgCampaignSends(firestore, siteOrgId) : null;
|
|
2588
|
+
const needsOrg = action !== 'proofOptions' && action !== 'test' && action !== 'renderPreview' && action !== 'preview';
|
|
2589
|
+
if (needsOrg && !orgSends) {
|
|
2590
|
+
return res.status(409).json({
|
|
2591
|
+
error: 'This site is not part of an organization, so it cannot hold ' + 'campaign emails.'
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
/*
|
|
2595
|
+
* THE CAMPAIGN A SEND JOINS has to be one this site can see.
|
|
2596
|
+
*
|
|
2597
|
+
* The id is a pointer the console chose from a picker, but the request
|
|
2598
|
+
* could carry any container in the org — including one placed only on a
|
|
2599
|
+
* sibling site, which this site's editors cannot read — or one that has
|
|
2600
|
+
* been deleted. Filing a send under either would put it where its own
|
|
2601
|
+
* editors cannot find it. Checked only for the actions that store it.
|
|
2602
|
+
*/ if (emailCampaignId && (action === 'draft' || action === 'schedule' || action === 'send')) {
|
|
2603
|
+
const container = await orgEmailCampaigns(firestore, siteOrgId).doc(emailCampaignId).get();
|
|
2604
|
+
if (!container.exists || container.get('deletedAt') != null || !campaignPlacedOnHost({
|
|
2605
|
+
visibleTo: container.get('visibleTo')
|
|
2606
|
+
}, hostId)) {
|
|
2607
|
+
return res.status(400).json({
|
|
2608
|
+
error: 'Unknown campaign'
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2541
2612
|
if (action === 'proofOptions') {
|
|
2542
2613
|
var _decoded_email;
|
|
2543
2614
|
/*
|
|
@@ -2565,7 +2636,7 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2565
2636
|
});
|
|
2566
2637
|
}
|
|
2567
2638
|
if (action === 'test') {
|
|
2568
|
-
var _decoded_email1,
|
|
2639
|
+
var _decoded_email1, _ref16, _ref17;
|
|
2569
2640
|
var _req_body20, _req_body21;
|
|
2570
2641
|
/*
|
|
2571
2642
|
* PROOF ONE EMAIL: as somebody, to somebody, from an identity — and
|
|
@@ -2577,7 +2648,7 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2577
2648
|
* to. Which identity it leaves on is not among them: that is resolved
|
|
2578
2649
|
* from the host document, and no request field reaches it.
|
|
2579
2650
|
*/ const ownEmail = String((_decoded_email1 = decoded.email) != null ? _decoded_email1 : '').trim().toLowerCase();
|
|
2580
|
-
const requestedTo = String((
|
|
2651
|
+
const requestedTo = String((_ref16 = (_req_body20 = req.body) == null ? void 0 : _req_body20.to) != null ? _ref16 : '').trim().toLowerCase();
|
|
2581
2652
|
const testEmail = requestedTo || ownEmail;
|
|
2582
2653
|
if (!testEmail) {
|
|
2583
2654
|
return res.status(400).json({
|
|
@@ -2618,7 +2689,7 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2618
2689
|
* taken from the request, so what a proof shows is what the audience
|
|
2619
2690
|
* document actually holds — a persona assembled from a request would
|
|
2620
2691
|
* demonstrate the composer's own typing rather than the merge.
|
|
2621
|
-
*/ const persona = await resolveProofPersona(hostId, String((
|
|
2692
|
+
*/ const persona = await resolveProofPersona(hostId, String((_ref17 = (_req_body21 = req.body) == null ? void 0 : _req_body21.personaEmail) != null ? _ref17 : ''));
|
|
2622
2693
|
const result = await performCampaignSend(_extends({
|
|
2623
2694
|
hostId,
|
|
2624
2695
|
subject,
|
|
@@ -2673,8 +2744,7 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2673
2744
|
cname: hostSnapshot.get('cname'),
|
|
2674
2745
|
subdomain: hostSnapshot.get('subdomain')
|
|
2675
2746
|
})) != null ? _hostPublicOrigin : '';
|
|
2676
|
-
const
|
|
2677
|
-
const branding = resolveBrandingProfile(orgForHost == null ? void 0 : orgForHost.org);
|
|
2747
|
+
const branding = resolveBrandingProfile(orgForSite == null ? void 0 : orgForSite.org);
|
|
2678
2748
|
/*
|
|
2679
2749
|
* Personalized for the REQUESTER, because a preview showing raw
|
|
2680
2750
|
* `{{firstName|there}}` tells a merchant nothing about what a recipient
|
|
@@ -2712,7 +2782,7 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2712
2782
|
} : {}));
|
|
2713
2783
|
}
|
|
2714
2784
|
if (action === 'preview') {
|
|
2715
|
-
var
|
|
2785
|
+
var _ref18, _ref19;
|
|
2716
2786
|
var _req_body22, _req_body23, _req_body24;
|
|
2717
2787
|
// Read-only, and it needs the same admin/editor role as a send: the
|
|
2718
2788
|
// audience size of someone else's site is not public information.
|
|
@@ -2721,8 +2791,8 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2721
2791
|
subject: subject || 'preview',
|
|
2722
2792
|
body: body || 'preview',
|
|
2723
2793
|
audience,
|
|
2724
|
-
segmentId: String((
|
|
2725
|
-
listId: String((
|
|
2794
|
+
segmentId: String((_ref18 = (_req_body22 = req.body) == null ? void 0 : _req_body22.segmentId) != null ? _ref18 : ''),
|
|
2795
|
+
listId: String((_ref19 = (_req_body23 = req.body) == null ? void 0 : _req_body23.listId) != null ? _ref19 : ''),
|
|
2726
2796
|
topicId: topicId || undefined,
|
|
2727
2797
|
emails: Array.isArray((_req_body24 = req.body) == null ? void 0 : _req_body24.emails) ? req.body.emails.map(String) : undefined,
|
|
2728
2798
|
templateScreenId: templateScreenId || undefined
|
|
@@ -2735,7 +2805,7 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2735
2805
|
return res.status(200).json(result);
|
|
2736
2806
|
}
|
|
2737
2807
|
if (action === 'followUp') {
|
|
2738
|
-
var
|
|
2808
|
+
var _ref20;
|
|
2739
2809
|
var _req_body25, _req_body26;
|
|
2740
2810
|
/*
|
|
2741
2811
|
* SEND AN EMAIL THAT HAS ALREADY GONE OUT TO THE PEOPLE IT HAS NOT
|
|
@@ -2751,14 +2821,14 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2751
2821
|
* `dryRun` rides through so the console can ask how many people are
|
|
2752
2822
|
* left before offering the button, and the answer comes from the code
|
|
2753
2823
|
* that would do the sending rather than from a count of its own.
|
|
2754
|
-
*/ const followUpId = String((
|
|
2824
|
+
*/ const followUpId = String((_ref20 = (_req_body25 = req.body) == null ? void 0 : _req_body25.campaignId) != null ? _ref20 : '');
|
|
2755
2825
|
if (!isDocumentId(followUpId)) {
|
|
2756
2826
|
return res.status(400).json({
|
|
2757
2827
|
error: 'Invalid campaignId'
|
|
2758
2828
|
});
|
|
2759
2829
|
}
|
|
2760
|
-
const sendSnapshot = await
|
|
2761
|
-
if (!sendSnapshot.exists) {
|
|
2830
|
+
const sendSnapshot = await orgSends.doc(followUpId).get();
|
|
2831
|
+
if (!sendSnapshot.exists || !sendIsOnHost(sendSnapshot, hostId)) {
|
|
2762
2832
|
return res.status(404).json({
|
|
2763
2833
|
error: 'Unknown email'
|
|
2764
2834
|
});
|
|
@@ -2794,16 +2864,16 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2794
2864
|
* escape on its own; `performCampaignSend` is the only thing that mails
|
|
2795
2865
|
* it, and only when somebody asks.
|
|
2796
2866
|
*=========================================*/ if (action === 'draft' || action === 'schedule' || action === 'update') {
|
|
2797
|
-
var
|
|
2867
|
+
var _ref21, _ref22, _targetSnapshot_get;
|
|
2798
2868
|
var _req_body27, _req_body28, _req_body29, _req_body30, _req_body31, _req_body32;
|
|
2799
2869
|
const scheduling = action === 'schedule';
|
|
2800
|
-
const sendAtMs = Number((
|
|
2870
|
+
const sendAtMs = Number((_ref21 = (_req_body27 = req.body) == null ? void 0 : _req_body27.sendAtMs) != null ? _ref21 : 0);
|
|
2801
2871
|
if (scheduling && (!Number.isFinite(sendAtMs) || sendAtMs <= Date.now())) {
|
|
2802
2872
|
return res.status(400).json({
|
|
2803
2873
|
error: 'Pick a future send time'
|
|
2804
2874
|
});
|
|
2805
2875
|
}
|
|
2806
|
-
const campaignId = String((
|
|
2876
|
+
const campaignId = String((_ref22 = (_req_body28 = req.body) == null ? void 0 : _req_body28.campaignId) != null ? _ref22 : '') || createResourceUid();
|
|
2807
2877
|
// AGL-1771: this branch WRITES, and it is the only campaign write that
|
|
2808
2878
|
// does not go through `performCampaignSend`'s guard. A `campaignId` of
|
|
2809
2879
|
// `a/b/c` scheduled the campaign at `campaigns/a/b/c` — which the
|
|
@@ -2815,8 +2885,17 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2815
2885
|
error: 'Invalid campaignId'
|
|
2816
2886
|
});
|
|
2817
2887
|
}
|
|
2818
|
-
const targetRef =
|
|
2888
|
+
const targetRef = orgSends.doc(campaignId);
|
|
2819
2889
|
const targetSnapshot = await targetRef.get();
|
|
2890
|
+
/*
|
|
2891
|
+
* An id that names a sibling site's send is refused, not adopted: the
|
|
2892
|
+
* merge below would otherwise rewrite that site's email under this
|
|
2893
|
+
* site's name.
|
|
2894
|
+
*/ if (targetSnapshot.exists && !sendIsOnHost(targetSnapshot, hostId)) {
|
|
2895
|
+
return res.status(404).json({
|
|
2896
|
+
error: 'Unknown email'
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2820
2899
|
const targetState = targetSnapshot.exists ? String((_targetSnapshot_get = targetSnapshot.get('status')) != null ? _targetSnapshot_get : '') : '';
|
|
2821
2900
|
/*==========================================
|
|
2822
2901
|
* WHAT IS ALREADY IN AN INBOX IS NOT EDITABLE.
|
|
@@ -2851,9 +2930,9 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2851
2930
|
error: 'Unknown email'
|
|
2852
2931
|
});
|
|
2853
2932
|
}
|
|
2854
|
-
await targetRef.set({
|
|
2933
|
+
await targetRef.set(_extends({}, campaignSendSiteStamp(hostId), {
|
|
2855
2934
|
displayName
|
|
2856
|
-
}, {
|
|
2935
|
+
}), {
|
|
2857
2936
|
merge: true
|
|
2858
2937
|
});
|
|
2859
2938
|
return res.status(200).json({
|
|
@@ -2861,7 +2940,7 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2861
2940
|
displayName
|
|
2862
2941
|
});
|
|
2863
2942
|
}
|
|
2864
|
-
await targetRef.set(_extends({
|
|
2943
|
+
await targetRef.set(_extends({}, campaignSendSiteStamp(hostId), {
|
|
2865
2944
|
subject,
|
|
2866
2945
|
body,
|
|
2867
2946
|
audience
|
|
@@ -2938,9 +3017,9 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2938
3017
|
});
|
|
2939
3018
|
}
|
|
2940
3019
|
if (action === 'cancel') {
|
|
2941
|
-
var
|
|
3020
|
+
var _ref23;
|
|
2942
3021
|
var _req_body33;
|
|
2943
|
-
const campaignId = String((
|
|
3022
|
+
const campaignId = String((_ref23 = (_req_body33 = req.body) == null ? void 0 : _req_body33.campaignId) != null ? _ref23 : '');
|
|
2944
3023
|
// AGL-1771: the ref used to be built one line ABOVE the `campaignId ?`
|
|
2945
3024
|
// check below, which defeated that check — `.doc('')` throws on an empty
|
|
2946
3025
|
// path segment, so the 400 this branch intends became a 500. Guarding
|
|
@@ -2950,18 +3029,18 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2950
3029
|
error: 'Not a scheduled campaign'
|
|
2951
3030
|
});
|
|
2952
3031
|
}
|
|
2953
|
-
const campaignRef =
|
|
3032
|
+
const campaignRef = orgSends.doc(campaignId);
|
|
2954
3033
|
const campaignSnapshot = await campaignRef.get();
|
|
2955
|
-
if (!(campaignSnapshot == null ? void 0 : campaignSnapshot.exists) || campaignSnapshot.get('status') !== 'scheduled') {
|
|
3034
|
+
if (!(campaignSnapshot == null ? void 0 : campaignSnapshot.exists) || !sendIsOnHost(campaignSnapshot, hostId) || campaignSnapshot.get('status') !== 'scheduled') {
|
|
2956
3035
|
return res.status(400).json({
|
|
2957
3036
|
error: 'Not a scheduled campaign'
|
|
2958
3037
|
});
|
|
2959
3038
|
}
|
|
2960
|
-
await campaignRef.set({
|
|
3039
|
+
await campaignRef.set(_extends({}, campaignSendSiteStamp(hostId), {
|
|
2961
3040
|
status: 'canceled',
|
|
2962
3041
|
canceledAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),
|
|
2963
3042
|
canceledBy: decoded.uid
|
|
2964
|
-
}, {
|
|
3043
|
+
}), {
|
|
2965
3044
|
merge: true
|
|
2966
3045
|
});
|
|
2967
3046
|
return res.status(200).json({
|
|
@@ -2970,7 +3049,7 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2970
3049
|
});
|
|
2971
3050
|
}
|
|
2972
3051
|
if (action === 'sendNow') {
|
|
2973
|
-
var
|
|
3052
|
+
var _ref24, _sendNowSnapshot_get, _ref25;
|
|
2974
3053
|
var _req_body34, _sendNowSnapshot_get1;
|
|
2975
3054
|
/*==========================================
|
|
2976
3055
|
* MAIL A DRAFTED OR SCHEDULED EMAIL, NOW.
|
|
@@ -2987,15 +3066,15 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
2987
3066
|
* lists, the same topic filtering, the same monthly reservation and the
|
|
2988
3067
|
* same hourly governor — because it is the same function the scheduled
|
|
2989
3068
|
* processor and the composer both call.
|
|
2990
|
-
*=========================================*/ const sendNowId = String((
|
|
3069
|
+
*=========================================*/ const sendNowId = String((_ref24 = (_req_body34 = req.body) == null ? void 0 : _req_body34.campaignId) != null ? _ref24 : '');
|
|
2991
3070
|
if (!isDocumentId(sendNowId)) {
|
|
2992
3071
|
return res.status(400).json({
|
|
2993
3072
|
error: 'Invalid campaignId'
|
|
2994
3073
|
});
|
|
2995
3074
|
}
|
|
2996
|
-
const sendNowRef =
|
|
3075
|
+
const sendNowRef = orgSends.doc(sendNowId);
|
|
2997
3076
|
const sendNowSnapshot = await sendNowRef.get();
|
|
2998
|
-
if (!sendNowSnapshot.exists) {
|
|
3077
|
+
if (!sendNowSnapshot.exists || !sendIsOnHost(sendNowSnapshot, hostId)) {
|
|
2999
3078
|
return res.status(404).json({
|
|
3000
3079
|
error: 'Unknown email'
|
|
3001
3080
|
});
|
|
@@ -3028,7 +3107,7 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
3028
3107
|
* beat — so there is nothing for a merchant to ask for here, and
|
|
3029
3108
|
* "Send now" on a send already in flight is a request made under a
|
|
3030
3109
|
* misunderstanding the answer should correct.
|
|
3031
|
-
*=========================================*/ const resumeRemaining = Math.floor(Number((
|
|
3110
|
+
*=========================================*/ const resumeRemaining = Math.floor(Number((_ref25 = (_sendNowSnapshot_get1 = sendNowSnapshot.get('resume')) == null ? void 0 : _sendNowSnapshot_get1.remaining) != null ? _ref25 : 0));
|
|
3032
3111
|
if (Number.isFinite(resumeRemaining) && resumeRemaining > 0) {
|
|
3033
3112
|
return res.status(409).json({
|
|
3034
3113
|
error: 'This email is already being sent, and the rest of it goes out ' + 'on its own. Sending it now would mail a second copy to ' + 'everyone it has already reached.'
|
|
@@ -3102,7 +3181,7 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
3102
3181
|
* Reaching the people an existing email has NOT reached is `followUp`,
|
|
3103
3182
|
* which takes no copy from the request at all and subtracts everyone the
|
|
3104
3183
|
* earlier sends recorded.
|
|
3105
|
-
*=========================================*/ const sendId = String((
|
|
3184
|
+
*=========================================*/ const sendId = String((_ref12 = (_req_body15 = req.body) == null ? void 0 : _req_body15.campaignId) != null ? _ref12 : '');
|
|
3106
3185
|
if (sendId) {
|
|
3107
3186
|
var _existing_get;
|
|
3108
3187
|
if (!isDocumentId(sendId)) {
|
|
@@ -3110,7 +3189,12 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
3110
3189
|
error: 'Invalid campaignId'
|
|
3111
3190
|
});
|
|
3112
3191
|
}
|
|
3113
|
-
const existing = await
|
|
3192
|
+
const existing = await orgSends.doc(sendId).get();
|
|
3193
|
+
if (existing.exists && !sendIsOnHost(existing, hostId)) {
|
|
3194
|
+
return res.status(404).json({
|
|
3195
|
+
error: 'Unknown email'
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3114
3198
|
const existingState = existing.exists ? String((_existing_get = existing.get('status')) != null ? _existing_get : '') : '';
|
|
3115
3199
|
if (existing.exists && existingState !== 'draft' && existingState !== 'scheduled') {
|
|
3116
3200
|
return res.status(409).json({
|
|
@@ -3123,12 +3207,12 @@ function storedSendOptionsFrom(snapshot, hostId, senderUid, followUp) {
|
|
|
3123
3207
|
subject,
|
|
3124
3208
|
body,
|
|
3125
3209
|
audience,
|
|
3126
|
-
segmentId: String((
|
|
3127
|
-
listId: String((
|
|
3210
|
+
segmentId: String((_ref13 = (_req_body16 = req.body) == null ? void 0 : _req_body16.segmentId) != null ? _ref13 : ''),
|
|
3211
|
+
listId: String((_ref14 = (_req_body17 = req.body) == null ? void 0 : _req_body17.listId) != null ? _ref14 : ''),
|
|
3128
3212
|
topicId: topicId || undefined,
|
|
3129
3213
|
emails: Array.isArray((_req_body18 = req.body) == null ? void 0 : _req_body18.emails) ? req.body.emails : undefined,
|
|
3130
3214
|
campaignId: sendId,
|
|
3131
|
-
experimentId: String((
|
|
3215
|
+
experimentId: String((_ref15 = (_req_body19 = req.body) == null ? void 0 : _req_body19.experimentId) != null ? _ref15 : ''),
|
|
3132
3216
|
templateScreenId: templateScreenId || undefined
|
|
3133
3217
|
}, plainText ? {
|
|
3134
3218
|
plainText
|