@aglyn/plugins-outreach 1.0.0-beta.178 → 1.0.0-beta.179

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.
@@ -18,7 +18,6 @@ import type { EmailState } from '@aglyn/aglyn/app-utils/email-state';
18
18
  import type { PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline';
19
19
  import type { OutreachMailboxNotice } from '../engine/mailbox-notice';
20
20
  import type { OpenedOutreachMailbox } from '../mailboxes/mailbox-transport';
21
- import type { OutreachClickTarget } from './click-link';
22
21
  import type { OutreachUnsubscribeTarget } from './unsubscribe-link';
23
22
  /**
24
23
  * EVERYTHING THE SENDING RUNTIME REACHES OUTSIDE ITS OWN DOCUMENTS (AGL-2981).
@@ -176,13 +175,14 @@ export interface OutreachRuntimeDeps {
176
175
  /** The signed one-click link for an enrollment, or `null` when none can be minted. */
177
176
  unsubscribeUrl(target: OutreachUnsubscribeTarget): string | null;
178
177
  /**
179
- * The signed tracking link for one link in one email, or `null` when none
180
- * can be minted (AGL-3239) — no console origin, no signing secret, or a
181
- * destination we will not sign.
178
+ * The short tracking link for a stored link's id (AGL-3239, AGL-3297), or
179
+ * `null` when none can be made — no HTTPS console origin. The sending
180
+ * runtime writes the `outreachLinks` document the id names before the
181
+ * email leaves.
182
182
  *
183
183
  * A `null` is not an error: the link goes out as the step wrote it, and
184
184
  * that click is not counted. An email whose links do not work is worse
185
185
  * than an email we cannot measure.
186
186
  */
187
- clickUrl(target: OutreachClickTarget): string | null;
187
+ clickLinkUrl(linkId: string): string | null;
188
188
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/runtime-deps.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { EmailState } from '@aglyn/aglyn/app-utils/email-state'\nimport type { PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport type { OutreachMailboxNotice } from '../engine/mailbox-notice'\nimport type { OpenedOutreachMailbox } from '../mailboxes/mailbox-transport'\nimport type { OutreachClickTarget } from './click-link'\nimport type { OutreachUnsubscribeTarget } from './unsubscribe-link'\n\n/**\n * EVERYTHING THE SENDING RUNTIME REACHES OUTSIDE ITS OWN DOCUMENTS (AGL-2981).\n *\n * The send and sync jobs and the unsubscribe route read and write Outreach's\n * collections through the Firestore they are handed, and reach everything\n * else through these: the mailbox's Gmail, the record system, the platform's\n * suppression and topic lists, the activity feed and the gates that decide\n * whether Outreach may act for a workspace at all. The platform's own are\n * `platform-runtime-deps.ts`; the emulator specs hand in a fake Gmail and\n * recorders, so no test reaches Google or sends mail.\n */\n\n/** A target an org activity line written by the runtime points at. */\nexport interface OutreachRuntimeActivityTarget {\n type: `${string}:${string}`\n id: string\n name: string\n}\n\n/**\n * What the runtime tells a mailbox's owner (AGL-3244): the notice the\n * engine composes the email from, and who it is for — the member who\n * connected the mailbox, and the organization's owners and admins beside\n * them, since a paused mailbox is the organization's problem too.\n */\nexport interface OutreachMailboxNoticeRequest extends OutreachMailboxNotice {\n orgId: string\n mailboxId: string\n /** The member whose mailbox it is. */\n connectedByUid: string\n}\n\n/**\n * The verdict on an address, for the record the person is (AGL-3245): what\n * the runtime wrote on a list, said on the lead and the contact that carry\n * the address. The platform finds the records; the runtime names none.\n */\nexport interface OutreachRecordEmailStamp {\n orgId: string\n email: string\n state: EmailState\n}\n\n/**\n * What a sequence produced, credited to the campaigns it is in (AGL-3254).\n *\n * Three doors into the platform's campaign attribution, all on the\n * platform's own join and none on the marketing plugin, which Outreach may\n * not import. Each is called once per enrollment per outcome, from the\n * state change that happens once; none throws, because the send, the reply\n * and the click have already happened and a lost credit understates a\n * campaign while a thrown one loses a step.\n */\nexport interface OutreachCampaignCredit {\n /**\n * Adds one to an outcome under every campaign named, in the campaign's\n * sequences report. `enrolled`, `sent`, `replied` and `converted` are the\n * runtime's to credit; `meetings` is credited by the booking door from\n * the touch {@link OutreachCampaignCredit.recordTouch} stamps.\n */\n credit(input: {\n hostId: string\n /**\n * The org whose campaigns these are, when the caller holds it — an\n * enrollment is an org record, so its routes do. The platform resolves\n * it from the site otherwise.\n */\n orgId?: string | null\n campaignIds: readonly string[]\n outcome: 'enrolled' | 'sent' | 'replied' | 'converted'\n atMs: number\n }): Promise<void>\n /**\n * Writes the conversion record the person's own page reads as \"where\n * this came from\": the sequence's campaign as the touch, on the lead or\n * the contact the enrollment names. Once per record, and never over a\n * record that already carries a web or email touch — the join's own\n * `create()` decides that.\n */\n attributeRecord(input: {\n hostId: string\n kind: 'lead' | 'contact'\n /** The lead's person key, or the contact's id. */\n refId: string\n campaignId: string\n sequenceId: string\n enrollmentId: string\n atMs: number\n }): Promise<void>\n /**\n * Stamps a click on a sequence email as the person's last campaign touch\n * on the site, so a booking or a form they go on to submit is credited to\n * the sequence's campaign by the door that credits every other one.\n */\n recordTouch(input: {\n hostId: string\n email: string\n campaignId: string\n sequenceId: string\n enrollmentId: string\n atMs: number\n }): Promise<void>\n}\n\nexport interface OutreachRuntimeDeps {\n firestore(): FirebaseFirestore.Firestore\n now(): number\n /** A source of numbers in `[0, 1)` — the scheduler's jitter. */\n random(): number\n /** Opens a mailbox's Gmail client from its sealed grant. */\n openMailbox(mailboxId: string): Promise<OpenedOutreachMailbox>\n /**\n * Why Outreach may not act for this workspace now, or `null` when it may:\n * the plugin switched on and entitled, and its release flag on for the\n * organization.\n */\n orgRefusal(orgId: string, org: Record<string, unknown>): Promise<string | null>\n /**\n * Why nothing may be sent as this member now — a lockdown of the platform,\n * the workspace or the member — or `null`.\n */\n sendRefusal(input: { orgId: string; org: Record<string, unknown>; uid: string }): Promise<string | null>\n /** The workspace's record system on the timeline seam, when a plugin keeps one. */\n timeline(): PluginRecordTimelineWriter | null\n /** The platform's campaign attribution, for what a sequence produced (AGL-3254). */\n campaignCredit: OutreachCampaignCredit\n logOrgActivity(\n orgId: string,\n actor: { uid: string | null; email?: string | null },\n action: string,\n target: OutreachRuntimeActivityTarget,\n ): Promise<void>\n /** Takes an address off one site's `sales` email topic. */\n optOutOfSalesTopic(input: { hostId: string; email: string }): Promise<void>\n /** Files a hard bounce on the platform's suppression list. */\n suppressBouncedEmail(input: { email: string; hostId: string | null }): Promise<void>\n /**\n * Stamps the verdict on the lead and the contact that carry the address\n * (AGL-3245), beside every write the runtime makes to a list. Never\n * throws: the list is the control, the stamp is what a person reads.\n */\n stampRecordEmailState(stamp: OutreachRecordEmailStamp): Promise<void>\n /**\n * Emails the mailbox's owner, and the organization's admins, that the\n * mailbox paused itself or needs reconnecting (AGL-3244), through the\n * platform's transactional sender. Called once per pause and once per\n * reconnect, by the code path that wrote it; a failure to send is the\n * platform's to log and never the runtime's to retry.\n */\n notifyMailboxOwner(notice: OutreachMailboxNoticeRequest): Promise<void>\n /** The signed one-click link for an enrollment, or `null` when none can be minted. */\n unsubscribeUrl(target: OutreachUnsubscribeTarget): string | null\n /**\n * The signed tracking link for one link in one email, or `null` when none\n * can be minted (AGL-3239) — no console origin, no signing secret, or a\n * destination we will not sign.\n *\n * A `null` is not an error: the link goes out as the step wrote it, and\n * that click is not counted. An email whose links do not work is worse\n * than an email we cannot measure.\n */\n clickUrl(target: OutreachClickTarget): string | null\n}\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAiHD,WA0DC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/runtime-deps.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { EmailState } from '@aglyn/aglyn/app-utils/email-state'\nimport type { PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport type { OutreachMailboxNotice } from '../engine/mailbox-notice'\nimport type { OpenedOutreachMailbox } from '../mailboxes/mailbox-transport'\nimport type { OutreachUnsubscribeTarget } from './unsubscribe-link'\n\n/**\n * EVERYTHING THE SENDING RUNTIME REACHES OUTSIDE ITS OWN DOCUMENTS (AGL-2981).\n *\n * The send and sync jobs and the unsubscribe route read and write Outreach's\n * collections through the Firestore they are handed, and reach everything\n * else through these: the mailbox's Gmail, the record system, the platform's\n * suppression and topic lists, the activity feed and the gates that decide\n * whether Outreach may act for a workspace at all. The platform's own are\n * `platform-runtime-deps.ts`; the emulator specs hand in a fake Gmail and\n * recorders, so no test reaches Google or sends mail.\n */\n\n/** A target an org activity line written by the runtime points at. */\nexport interface OutreachRuntimeActivityTarget {\n type: `${string}:${string}`\n id: string\n name: string\n}\n\n/**\n * What the runtime tells a mailbox's owner (AGL-3244): the notice the\n * engine composes the email from, and who it is for — the member who\n * connected the mailbox, and the organization's owners and admins beside\n * them, since a paused mailbox is the organization's problem too.\n */\nexport interface OutreachMailboxNoticeRequest extends OutreachMailboxNotice {\n orgId: string\n mailboxId: string\n /** The member whose mailbox it is. */\n connectedByUid: string\n}\n\n/**\n * The verdict on an address, for the record the person is (AGL-3245): what\n * the runtime wrote on a list, said on the lead and the contact that carry\n * the address. The platform finds the records; the runtime names none.\n */\nexport interface OutreachRecordEmailStamp {\n orgId: string\n email: string\n state: EmailState\n}\n\n/**\n * What a sequence produced, credited to the campaigns it is in (AGL-3254).\n *\n * Three doors into the platform's campaign attribution, all on the\n * platform's own join and none on the marketing plugin, which Outreach may\n * not import. Each is called once per enrollment per outcome, from the\n * state change that happens once; none throws, because the send, the reply\n * and the click have already happened and a lost credit understates a\n * campaign while a thrown one loses a step.\n */\nexport interface OutreachCampaignCredit {\n /**\n * Adds one to an outcome under every campaign named, in the campaign's\n * sequences report. `enrolled`, `sent`, `replied` and `converted` are the\n * runtime's to credit; `meetings` is credited by the booking door from\n * the touch {@link OutreachCampaignCredit.recordTouch} stamps.\n */\n credit(input: {\n hostId: string\n /**\n * The org whose campaigns these are, when the caller holds it — an\n * enrollment is an org record, so its routes do. The platform resolves\n * it from the site otherwise.\n */\n orgId?: string | null\n campaignIds: readonly string[]\n outcome: 'enrolled' | 'sent' | 'replied' | 'converted'\n atMs: number\n }): Promise<void>\n /**\n * Writes the conversion record the person's own page reads as \"where\n * this came from\": the sequence's campaign as the touch, on the lead or\n * the contact the enrollment names. Once per record, and never over a\n * record that already carries a web or email touch — the join's own\n * `create()` decides that.\n */\n attributeRecord(input: {\n hostId: string\n kind: 'lead' | 'contact'\n /** The lead's person key, or the contact's id. */\n refId: string\n campaignId: string\n sequenceId: string\n enrollmentId: string\n atMs: number\n }): Promise<void>\n /**\n * Stamps a click on a sequence email as the person's last campaign touch\n * on the site, so a booking or a form they go on to submit is credited to\n * the sequence's campaign by the door that credits every other one.\n */\n recordTouch(input: {\n hostId: string\n email: string\n campaignId: string\n sequenceId: string\n enrollmentId: string\n atMs: number\n }): Promise<void>\n}\n\nexport interface OutreachRuntimeDeps {\n firestore(): FirebaseFirestore.Firestore\n now(): number\n /** A source of numbers in `[0, 1)` — the scheduler's jitter. */\n random(): number\n /** Opens a mailbox's Gmail client from its sealed grant. */\n openMailbox(mailboxId: string): Promise<OpenedOutreachMailbox>\n /**\n * Why Outreach may not act for this workspace now, or `null` when it may:\n * the plugin switched on and entitled, and its release flag on for the\n * organization.\n */\n orgRefusal(orgId: string, org: Record<string, unknown>): Promise<string | null>\n /**\n * Why nothing may be sent as this member now — a lockdown of the platform,\n * the workspace or the member — or `null`.\n */\n sendRefusal(input: { orgId: string; org: Record<string, unknown>; uid: string }): Promise<string | null>\n /** The workspace's record system on the timeline seam, when a plugin keeps one. */\n timeline(): PluginRecordTimelineWriter | null\n /** The platform's campaign attribution, for what a sequence produced (AGL-3254). */\n campaignCredit: OutreachCampaignCredit\n logOrgActivity(\n orgId: string,\n actor: { uid: string | null; email?: string | null },\n action: string,\n target: OutreachRuntimeActivityTarget,\n ): Promise<void>\n /** Takes an address off one site's `sales` email topic. */\n optOutOfSalesTopic(input: { hostId: string; email: string }): Promise<void>\n /** Files a hard bounce on the platform's suppression list. */\n suppressBouncedEmail(input: { email: string; hostId: string | null }): Promise<void>\n /**\n * Stamps the verdict on the lead and the contact that carry the address\n * (AGL-3245), beside every write the runtime makes to a list. Never\n * throws: the list is the control, the stamp is what a person reads.\n */\n stampRecordEmailState(stamp: OutreachRecordEmailStamp): Promise<void>\n /**\n * Emails the mailbox's owner, and the organization's admins, that the\n * mailbox paused itself or needs reconnecting (AGL-3244), through the\n * platform's transactional sender. Called once per pause and once per\n * reconnect, by the code path that wrote it; a failure to send is the\n * platform's to log and never the runtime's to retry.\n */\n notifyMailboxOwner(notice: OutreachMailboxNoticeRequest): Promise<void>\n /** The signed one-click link for an enrollment, or `null` when none can be minted. */\n unsubscribeUrl(target: OutreachUnsubscribeTarget): string | null\n /**\n * The short tracking link for a stored link's id (AGL-3239, AGL-3297), or\n * `null` when none can be made — no HTTPS console origin. The sending\n * runtime writes the `outreachLinks` document the id names before the\n * email leaves.\n *\n * A `null` is not an error: the link goes out as the step wrote it, and\n * that click is not counted. An email whose links do not work is worse\n * than an email we cannot measure.\n */\n clickLinkUrl(linkId: string): string | null\n}\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAgHD,WA2DC"}
@@ -35,6 +35,7 @@ import { GmailTransportError, isReconnectRequired } from "../transport/gmail-err
35
35
  import { Rfc5322MessageError } from "../transport/rfc5322.js";
36
36
  import { sendComposedOutreachEmail } from "../transport/send-message.js";
37
37
  import { creditOutreachFirstSend } from "./campaign-credit.js";
38
+ import { newOutreachLinkId, outreachStoredLink } from "./click-link.js";
38
39
  import { applyOutreachEvent } from "./enrollment-events.js";
39
40
  import { outreachSendDigest, withRecentSend } from "./mailbox-health-store.js";
40
41
  import { noteOutreachMailboxReconnectRequired } from "./mailbox-notices.js";
@@ -647,15 +648,27 @@ async function runEmailStep(deps, firestore, run, input) {
647
648
  /*
648
649
  * Click tracking is the sequence's own setting (AGL-3239) and is read
649
650
  * here, per send, rather than once per run: a member turning it off
650
- * applies to the next email, not to the next hour. A link the minter
651
- * cannot sign is left exactly as the step wrote it.
652
- */ const rewriteLink = sequence.settings.trackClicks ? (link)=>deps.clickUrl({
651
+ * applies to the next email, not to the next hour. Each link becomes a
652
+ * short link (AGL-3297) whose document is written below, before the email
653
+ * leaves; a link that cannot be made is left exactly as the step wrote it.
654
+ */ const shortLinks = [];
655
+ const rewriteLink = sequence.settings.trackClicks ? (link)=>{
656
+ const doc = outreachStoredLink({
653
657
  orgId: run.orgId,
654
658
  enrollmentId: enrollment.id,
655
659
  stepIndex: enrollment.stepIndex,
656
660
  linkIndex: link.index,
657
661
  url: link.url
658
- }) : null;
662
+ }, nowMs);
663
+ const id = newOutreachLinkId();
664
+ const url = doc ? deps.clickLinkUrl(id) : null;
665
+ if (!doc || !url) return null;
666
+ shortLinks.push({
667
+ id,
668
+ doc
669
+ });
670
+ return url;
671
+ } : null;
659
672
  const composed = composeOutreachEmail({
660
673
  sequence,
661
674
  enrollment,
@@ -714,6 +727,24 @@ async function runEmailStep(deps, firestore, run, input) {
714
727
  detail: `Paused before sending: this contact has nothing for ${fields}. Fill it in and resume.`
715
728
  }, 'failed');
716
729
  }
730
+ /*
731
+ * The short links' documents, before anything is claimed or sent: a link
732
+ * in an email that already left must resolve on the first click. A write
733
+ * that fails holds the send for the next run, which mints new ones — an
734
+ * orphaned document is inert, an email whose links go nowhere is not.
735
+ */ if (shortLinks.length) {
736
+ try {
737
+ const batch = firestore.batch();
738
+ for (const link of shortLinks){
739
+ batch.create(firestore.collection(OUTREACH_COLLECTIONS.links).doc(link.id), link.doc);
740
+ }
741
+ await batch.commit();
742
+ } catch (error) {
743
+ console.error('[outreach] the short links could not be stored; the send waits for the next run', error);
744
+ report.held += 1;
745
+ return;
746
+ }
747
+ }
717
748
  // Claimed, then sent, then recorded.
718
749
  const domain = sender.address.slice(sender.address.lastIndexOf('@') + 1);
719
750
  const local = randomUUID();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/send-job.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { normalizeCrmEmailTemplate } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { randomUUID } from 'node:crypto'\nimport { composeOutreachEmail } from '../engine/compose'\nimport { planOutreachStepCompletion, type OutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport { evaluateOutreachGates, type OutreachGateBlock, type OutreachGateLookups } from '../engine/gates'\nimport {\n outreachSentToday,\n outreachTickAllowance,\n selectDueOutreachEnrollments,\n type OutreachDueCandidate,\n} from '../engine/sending-capacity'\nimport { readOutreachEnrollCandidates, type OutreachEnrollCandidate } from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport { outreachEffectiveDailyCap, outreachLocalDay } from '../mailboxes/mailbox-settings'\nimport type { OpenedOutreachMailbox } from '../mailboxes/mailbox-transport'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachComplianceSettingsDocument,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachStepRecord,\n type OutreachTaskStep,\n} from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachEnrollmentLink,\n outreachEnrollmentPerson,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { Rfc5322MessageError } from '../transport/rfc5322'\nimport { sendComposedOutreachEmail } from '../transport/send-message'\nimport { creditOutreachFirstSend } from './campaign-credit'\nimport { applyOutreachEvent } from './enrollment-events'\nimport { outreachSendDigest, withRecentSend } from './mailbox-health-store'\nimport { noteOutreachMailboxReconnectRequired } from './mailbox-notices'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { fileOutreachEmail, fileOutreachTask, OUTREACH_TASK_KIND_TO_RECORD } from './timeline'\nimport { outreachListUnsubscribe } from './unsubscribe-link'\n\n/*==========================================\n * THE SEND JOB (AGL-2981): every fifteen minutes, on the console.\n *\n * ## What comes due\n *\n * One collection-group read finds every active enrollment whose next step\n * is due, oldest first, bounded per run. They are grouped by workspace and\n * mailbox, and each group is judged before anything is sent:\n *\n * - the workspace: Outreach switched on, entitled and released to it;\n * - the mailbox: an enrollment whose mailbox is gone, disconnected, or whose\n * member left the organization is STOPPED — nothing will ever send it, and\n * leaving it active would show a sequence that is not running. A paused\n * mailbox, or one waiting to be reconnected, holds its enrollments;\n * - the member it sends as: a lockdown of the platform, the workspace or the\n * member holds everything.\n *\n * ## How much goes\n *\n * The engine's per-run allowance — the day's cap and ramp, spread across\n * the window, at most five a run — picks the enrollments (follow-ups first);\n * a task step is due whenever its time comes and spends no allowance.\n *\n * ## One send, exactly once\n *\n * Each email is CLAIMED before it is sent: a transaction over the enrollment\n * and its mailbox checks the enrollment is still active on the same step\n * with no live claim, and that the mailbox's day has room under its cap,\n * then writes the claim and reserves the day's count. Two runs racing for\n * one enrollment write one claim; two racing for one mailbox's last slot\n * reserve it once. The claim carries the `Message-ID` minted for the send,\n * so a claim a run died holding is settled by finding the message in the\n * mailbox rather than by sending it a second time. A send that fails hands\n * its reservation back.\n *\n * Before it is claimed, every gate is asked again — the do-not-contact\n * list, the suppression lists, the sales topic, the roster, the other\n * sequences — because a person enrolled on Monday can unsubscribe by\n * Thursday. A lookup that could not be made holds the send; a gate that\n * refuses stops the enrollment, saying why.\n *==========================================*/\n\nexport { OUTREACH_SEND_JOB_ID } from '../constants/runtime-jobs'\n\n/** The most due enrollments one run reads. */\nexport const OUTREACH_SEND_SCAN_LIMIT = 300\n\n/** How long a claim holds before a later run settles it. */\nexport const OUTREACH_CLAIM_STALE_MS = 10 * 60_000\n\n/** What one run did, for the tick's answer. */\nexport interface OutreachSendReport {\n [key: string]: number\n due: number\n sent: number\n tasks: number\n /** Stopped by a gate or an unusable mailbox. */\n stopped: number\n /** Paused for the rep to fix, or failed for good. */\n failed: number\n /** Due, and left for a later run: the allowance, a lookup, a hold. */\n held: number\n /** Claims a dead run left, settled. */\n recovered: number\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\ninterface MailboxRun {\n orgId: string\n org: Record<string, unknown>\n mailbox: OutreachMailbox\n settings: OutreachComplianceSettingsDocument\n report: OutreachSendReport\n deadlineMs: number\n opened: OpenedOutreachMailbox | null\n /** Set once the mailbox itself cannot send this run. */\n stopped: boolean\n templates: Map<string, string | null>\n}\n\nconst blankReport = (): OutreachSendReport => ({\n due: 0,\n sent: 0,\n tasks: 0,\n stopped: 0,\n failed: 0,\n held: 0,\n recovered: 0,\n})\n\n/** Runs the send job once — see the module note. */\nexport async function runOutreachSendJob(\n deps: OutreachRuntimeDeps,\n context: { nowMs: number; deadlineMs: number },\n): Promise<OutreachSendReport> {\n const firestore = deps.firestore()\n const report = blankReport()\n const due = await firestore\n .collectionGroup(OUTREACH_COLLECTIONS.enrollments)\n .where('status', '==', 'active')\n .where('nextDueAtMs', '<=', context.nowMs)\n .orderBy('nextDueAtMs')\n .limit(OUTREACH_SEND_SCAN_LIMIT)\n .get()\n report.due = due.size\n const byOrg = new Map<string, Map<string, OutreachEnrollment[]>>()\n for (const doc of due.docs) {\n const orgId = doc.ref.parent.parent?.id\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!orgId || !enrollment) continue\n const mailboxes = byOrg.get(orgId) ?? new Map<string, OutreachEnrollment[]>()\n mailboxes.set(enrollment.mailboxId, [...(mailboxes.get(enrollment.mailboxId) ?? []), enrollment])\n byOrg.set(orgId, mailboxes)\n }\n\n for (const [orgId, mailboxes] of byOrg) {\n const waiting = [...mailboxes.values()].reduce((sum, list) => sum + list.length, 0)\n if (deps.now() >= context.deadlineMs) {\n report.held += waiting\n continue\n }\n const orgSnapshot = await firestore.collection('orgs').doc(orgId).get()\n const org = (orgSnapshot.exists ? orgSnapshot.data() : null) as Record<string, unknown> | null\n if (!org || (await deps.orgRefusal(orgId, org))) {\n report.held += waiting\n continue\n }\n const settings = await readOutreachComplianceSettingsDoc(firestore, orgId)\n for (const [mailboxId, enrollments] of mailboxes) {\n await runMailbox(deps, firestore, { orgId, org, mailboxId, enrollments, settings, report, context })\n }\n }\n return report\n}\n\n/** The reason an unusable mailbox stops an enrollment, or `null` for a usable one. */\nasync function unusableMailbox(\n firestore: Firestore,\n orgId: string,\n mailbox: OutreachMailbox | null,\n): Promise<string | null> {\n if (!mailbox) return 'The mailbox this sequence sends from was removed.'\n if (mailbox.status === 'disconnected') return 'The mailbox this sequence sends from was disconnected.'\n const member = await firestore.collection('orgs').doc(orgId).collection('members').doc(mailbox.connectedByUid).get()\n return member.exists\n ? null\n : 'The member who connected the mailbox this sequence sends from is no longer in the organization.'\n}\n\nasync function runMailbox(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n input: {\n orgId: string\n org: Record<string, unknown>\n mailboxId: string\n enrollments: OutreachEnrollment[]\n settings: OutreachComplianceSettingsDocument\n report: OutreachSendReport\n context: { nowMs: number; deadlineMs: number }\n },\n): Promise<void> {\n const { orgId, org, report, context } = input\n const nowMs = context.nowMs\n const snapshot = await mailboxRef(firestore, orgId, input.mailboxId).get()\n const mailbox = readStoredOutreachMailbox(input.mailboxId, snapshot.exists ? snapshot.data() : undefined)\n const unusable = await unusableMailbox(firestore, orgId, mailbox)\n if (unusable || !mailbox) {\n for (const enrollment of input.enrollments) {\n const outcome = await applyOutreachEvent(firestore, {\n orgId,\n enrollmentId: enrollment.id,\n event: { type: 'stop', atMs: nowMs, byUid: null, reason: 'gate', detail: unusable },\n nowMs,\n })\n if (outcome.changed) report.stopped += 1\n }\n return\n }\n if (mailbox.status !== 'connected' || (await deps.sendRefusal({ orgId, org, uid: mailbox.connectedByUid }))) {\n report.held += input.enrollments.length\n return\n }\n\n const run: MailboxRun = {\n orgId,\n org,\n mailbox,\n settings: input.settings,\n report,\n deadlineMs: context.deadlineMs,\n opened: null,\n stopped: false,\n templates: new Map(),\n }\n const sequenceIds = [...new Set(input.enrollments.map((enrollment) => enrollment.sequenceId))]\n const sequenceSnapshots = await firestore.getAll(\n ...sequenceIds.map((id) => outreachOrgCollection(firestore, orgId, 'sequences').doc(id)),\n )\n const sequences = new Map<string, OutreachSequence>()\n sequenceSnapshots.forEach((sequenceSnapshot, index) => {\n const sequence = readStoredOutreachSequence(\n sequenceIds[index],\n sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined,\n )\n if (sequence) sequences.set(sequence.id, sequence)\n })\n\n const candidates: OutreachDueCandidate[] = []\n for (const enrollment of input.enrollments) {\n const sequence = sequences.get(enrollment.sequenceId)\n const claim = enrollment.sendClaim\n if (claim && nowMs - claim.atMs < OUTREACH_CLAIM_STALE_MS) continue\n if (claim && sequence) {\n if (await recoverClaim(deps, firestore, run, enrollment, sequence)) report.recovered += 1\n continue\n }\n if (sequence) candidates.push({ enrollment, sequence })\n }\n\n const allowance = outreachTickAllowance({ mailbox, nowMs })\n const selection = selectDueOutreachEnrollments({ candidates, mailbox, nowMs, allowance: allowance.allowance })\n report.held += selection.deferred.length\n\n for (const candidate of selection.tasks) {\n if (deps.now() >= context.deadlineMs) {\n report.held += 1\n continue\n }\n if (await runTaskStep(deps, firestore, run, candidate)) report.tasks += 1\n }\n if (!selection.emails.length) return\n\n // The people behind the emails, read once per site: the contact and its\n // company, and every lookup the gates decide on.\n const people = new Map<string, OutreachEnrollCandidate>()\n const lookups = new Map<string, OutreachGateLookups>()\n const byHost = new Map<string, OutreachEnrollment[]>()\n for (const candidate of selection.emails) {\n const enrollment = candidate.enrollment as OutreachEnrollment\n byHost.set(enrollment.hostId, [...(byHost.get(enrollment.hostId) ?? []), enrollment])\n }\n const siteNames = new Map<string, string>()\n for (const [hostId, enrollments] of byHost) {\n const contactGroupId = consentGroupForHost(org, hostId).groupId\n // Each by the record it names — the contact, or the lead while the\n // person is one (AGL-3234) — keyed by that record's id.\n const [candidatesRead, lookupsRead, host] = await Promise.all([\n readOutreachEnrollCandidates(firestore, {\n orgId,\n hostId,\n contactGroupId,\n people: enrollments.map(outreachEnrollmentPerson),\n }),\n readOutreachGateLookups(firestore, {\n orgId,\n hostId,\n people: enrollments.map((enrollment) => {\n const person = outreachEnrollmentPerson(enrollment)\n return {\n personId: person.id,\n contactId: person.kind === 'contact' ? person.id : null,\n leadId: person.kind === 'lead' ? person.id : null,\n email: enrollment.email,\n }\n }),\n }),\n firestore.collection('hosts').doc(hostId).get(),\n ])\n for (const person of candidatesRead) people.set(person.personId, person)\n for (const [personId, answer] of lookupsRead) lookups.set(personId, answer)\n siteNames.set(hostId, host.exists ? String(host.get('name') ?? '') : '')\n }\n\n for (const candidate of selection.emails) {\n if (run.stopped || deps.now() >= context.deadlineMs) {\n report.held += 1\n continue\n }\n const enrollment = candidate.enrollment as OutreachEnrollment\n const personId = outreachEnrollmentPerson(enrollment).id\n await runEmailStep(deps, firestore, run, {\n enrollment,\n sequence: candidate.sequence as OutreachSequence,\n person: people.get(personId) ?? null,\n lookups: lookups.get(personId) ?? null,\n siteName: siteNames.get(enrollment.hostId) ?? '',\n })\n }\n}\n\n/** Any lookup the runtime could not make: the send waits for a run that can. */\nfunction uncheckable(lookups: OutreachGateLookups | null): boolean {\n return !lookups || Object.values(lookups).some((value) => value === null)\n}\n\n/** The event a gate's refusal ends an enrollment with. */\nfunction gateEvent(blocks: readonly OutreachGateBlock[], atMs: number): OutreachEnrollmentEvent {\n const optedOut = blocks.find((block) => block.code === 'do_not_contact' || block.code === 'sales_opted_out')\n if (optedOut) return { type: 'opt_out', atMs, reason: 'do_not_contact', detail: optedOut.reason }\n return { type: 'stop', atMs, byUid: null, reason: 'gate', detail: blocks.map((block) => block.reason).join(' ') }\n}\n\n/** The sender line a mailbox's mail goes out with. */\nconst senderOf = (mailbox: OutreachMailbox) => ({\n address: mailbox.sendAs || mailbox.email,\n name: mailbox.displayName || null,\n})\n\nasync function openClient(deps: OutreachRuntimeDeps, run: MailboxRun) {\n const opened = (run.opened ??= await deps.openMailbox(run.mailbox.id))\n if (opened.ok === true) return opened.client\n run.stopped = true\n if (opened.reason !== 'not-configured') {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId: run.orgId,\n mailboxId: run.mailbox.id,\n errorCode: opened.reason,\n nowMs: deps.now(),\n })\n }\n return null\n}\n\n/**\n * Claims one step: the enrollment still active on the same step with no\n * live claim, and — for an email — a slot under the mailbox's cap today,\n * reserved in the same transaction.\n */\nasync function claimStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n enrollment: OutreachEnrollment,\n messageId: string | null,\n): Promise<'claimed' | 'taken' | 'cap'> {\n const enrollmentRef = outreachOrgCollection(firestore, run.orgId, 'enrollments').doc(enrollment.id)\n const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id)\n const nowMs = deps.now()\n return firestore.runTransaction(async (transaction) => {\n const [current, box] = await Promise.all([transaction.get(enrollmentRef), transaction.get(boxRef)])\n const stored = readStoredOutreachEnrollment(enrollment.id, current.exists ? current.data() : undefined)\n const claim = stored?.sendClaim\n if (\n !stored ||\n stored.status !== 'active' ||\n stored.stepIndex !== enrollment.stepIndex ||\n stored.nextDueAtMs === null ||\n stored.nextDueAtMs > nowMs ||\n (claim && nowMs - claim.atMs < OUTREACH_CLAIM_STALE_MS)\n ) {\n return 'taken'\n }\n if (messageId) {\n const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.exists ? box.data() : undefined)\n if (!mailbox || mailbox.status !== 'connected') return 'taken'\n const sent = outreachSentToday(mailbox.health, nowMs, mailbox.timezone)\n if (sent >= outreachEffectiveDailyCap(mailbox, nowMs)) return 'cap'\n transaction.update(boxRef, {\n 'health.sentToday': sent + 1,\n 'health.sentOnDay': outreachLocalDay(nowMs, mailbox.timezone),\n })\n }\n transaction.update(enrollmentRef, {\n sendClaim: { token: randomUUID(), atMs: nowMs, stepIndex: enrollment.stepIndex, messageId },\n updatedAtMs: nowMs,\n })\n return 'claimed'\n })\n}\n\n/** Drops a claim, and hands an email's reservation back to the mailbox's day. */\nasync function releaseClaim(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n enrollment: Pick<OutreachEnrollment, 'id'>,\n reserved: boolean,\n event: OutreachEnrollmentEvent | null = null,\n): Promise<void> {\n const nowMs = deps.now()\n await applyOutreachEvent(firestore, {\n orgId: run.orgId,\n enrollmentId: enrollment.id,\n event,\n extra: () => ({ sendClaim: null }),\n nowMs,\n })\n if (!reserved) return\n const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id)\n await firestore.runTransaction(async (transaction) => {\n const box = await transaction.get(boxRef)\n const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.exists ? box.data() : undefined)\n if (!mailbox || mailbox.health?.sentOnDay !== outreachLocalDay(nowMs, mailbox.timezone)) return\n transaction.update(boxRef, { 'health.sentToday': Math.max(0, (Number(mailbox.health.sentToday) || 0) - 1) })\n })\n}\n\n/**\n * Records a step that ran: the engine's completion patch, the claim\n * cleared, the step's record kept — and for an email the mailbox's send on\n * its day, its last-sent time and its window of recent sends. Refuses\n * (answers false) when the claim is no longer this run's.\n */\nasync function completeStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n input: {\n enrollment: OutreachEnrollment\n sequence: OutreachSequence\n record: OutreachStepRecord\n sent: { messageId: string; threadId: string; subject: string } | null\n claimMessageId: string | null\n },\n): Promise<boolean> {\n const enrollmentRef = outreachOrgCollection(firestore, run.orgId, 'enrollments').doc(input.enrollment.id)\n const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id)\n const nowMs = deps.now()\n /*\n * Whether this is the FIRST email this person has had from the sequence\n * (AGL-3239), decided from the same read the step is recorded against and\n * so re-decided on every transaction retry. It is what `stats.people`\n * counts: the denominator of every engagement rate, and the thing\n * `stats.sent` is not, since one person takes several steps.\n */\n let firstEmail = false\n const completed = await firestore.runTransaction(async (transaction) => {\n const [current, box] = await Promise.all([\n transaction.get(enrollmentRef),\n input.sent ? transaction.get(boxRef) : Promise.resolve(null),\n ])\n const stored = readStoredOutreachEnrollment(input.enrollment.id, current.exists ? current.data() : undefined)\n if (!stored?.sendClaim || stored.sendClaim.messageId !== input.claimMessageId) return false\n if (stored.stepIndex !== input.enrollment.stepIndex) return false\n // The step RAN, whatever happened to the enrollment while it did — a\n // member paused it, a reply stopped it — so it is recorded as run, and a\n // resume never sends it twice.\n firstEmail =\n input.sent !== null &&\n !(stored.stepRecords ?? []).some((record) => record.kind === 'email')\n const plan = planOutreachStepCompletion({\n enrollment: { ...stored, status: 'active' },\n sequence: input.sequence,\n mailbox: run.mailbox,\n completedAtMs: nowMs,\n sent: input.sent,\n random: deps.random,\n })\n const patch: Record<string, unknown> = {\n sendClaim: null,\n stepRecords: [...(stored.stepRecords ?? []), input.record].slice(-20),\n updatedAtMs: nowMs,\n ...(plan.patch ?? {}),\n }\n if (stored.status !== 'active') {\n patch['status'] = stored.status\n if (stored.status !== 'paused') patch['nextDueAtMs'] = null\n }\n transaction.update(enrollmentRef, patch)\n if (input.sent && box?.exists) {\n const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.data())\n if (mailbox) {\n const day = outreachLocalDay(nowMs, mailbox.timezone)\n const counts = mailbox.health?.daily?.[day] ?? { sent: 0, bounces: 0, replies: 0 }\n transaction.update(boxRef, {\n 'health.lastSentAtMs': nowMs,\n 'health.lastErrorAtMs': null,\n 'health.lastErrorCode': null,\n [`health.daily.${day}`]: { ...counts, sent: (Number(counts.sent) || 0) + 1 },\n 'health.recentSends': withRecentSend(mailbox.health?.recentSends, {\n id: outreachSendDigest(input.sent.messageId),\n atMs: nowMs,\n bounced: false,\n }),\n updatedAtMs: nowMs,\n })\n }\n }\n return true\n })\n if (completed && input.sent) {\n await bumpOutreachSequenceStats(firestore, {\n orgId: run.orgId,\n sequenceId: input.sequence.id,\n firstEmail,\n trackedLinks: input.record.links?.length ?? 0,\n })\n }\n return completed\n}\n\n/**\n * The sequence's send counters (AGL-3239), written outside the enrollment's\n * transaction.\n *\n * One sequence holds every enrollment, so a transaction over it would\n * serialise every recipient against every other; `FieldValue.increment` is a\n * blind write with nothing to contend on. It is bookkeeping beside the act,\n * so a failure is logged and the send stands — the email has already left.\n *\n * `clickTracked` is stamped by the first send that actually carried a\n * rewritten link, never by the SETTING: a sequence with the setting on whose\n * emails have no links in them measured nothing, and the report must say\n * \"not measured\" for it rather than publish a 0% click rate.\n */\nasync function bumpOutreachSequenceStats(\n firestore: Firestore,\n input: { orgId: string; sequenceId: string; firstEmail: boolean; trackedLinks: number },\n): Promise<void> {\n if (!input.sequenceId) return\n await outreachOrgCollection(firestore, input.orgId, 'sequences')\n .doc(input.sequenceId)\n .update({\n 'stats.sent': FieldValue.increment(1),\n ...(input.firstEmail ? { 'stats.people': FieldValue.increment(1) } : {}),\n ...(input.trackedLinks > 0 ? { 'stats.clickTracked': true } : {}),\n })\n .catch((error: unknown) => {\n console.warn('[outreach] a send could not be counted on its sequence', error)\n })\n}\n\n/** One task step: the rep's task on the contact, then the step recorded. */\nasync function runTaskStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n candidate: OutreachDueCandidate,\n): Promise<boolean> {\n const enrollment = candidate.enrollment as OutreachEnrollment\n const sequence = candidate.sequence as OutreachSequence\n const step = sequence.steps[enrollment.stepIndex] as OutreachTaskStep\n if ((await claimStep(deps, firestore, run, enrollment, null)) !== 'claimed') return false\n const nowMs = deps.now()\n let taskId: string | null\n try {\n const written = await fileOutreachTask(deps, {\n orgId: run.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: `step:${enrollment.id}:${step.id}`,\n title: step.title,\n notes: `From sequence step ${enrollment.stepIndex + 1}.`,\n kind: OUTREACH_TASK_KIND_TO_RECORD[step.taskKind] ?? 'todo',\n dueAtMs: nowMs,\n assigneeUid: run.mailbox.connectedByUid,\n })\n taskId = written?.ok ? written.id : null\n if (written?.ok === false) console.warn(`[outreach] a task step's task was not filed: ${written.error}`)\n } catch (error) {\n // The record system failed rather than refused: the step waits for a\n // run that can file its task.\n console.error('[outreach] filing a task step failed; the step waits', error)\n await releaseClaim(deps, firestore, run, enrollment, false)\n return false\n }\n return completeStep(deps, firestore, run, {\n enrollment,\n sequence,\n record: { stepIndex: enrollment.stepIndex, stepId: step.id, kind: 'task', atMs: nowMs, taskId },\n sent: null,\n claimMessageId: null,\n })\n}\n\n/** A CRM template's body, read once per run. */\nasync function templateBody(firestore: Firestore, run: MailboxRun, templateId: string): Promise<string | null> {\n if (!run.templates.has(templateId)) {\n const snapshot = await firestore\n .collection('orgs')\n .doc(run.orgId)\n .collection(CRM_COLLECTIONS.emailTemplates)\n .doc(templateId)\n .get()\n run.templates.set(\n templateId,\n snapshot.exists ? normalizeCrmEmailTemplate(snapshot.data() as Record<string, unknown>).body : null,\n )\n }\n return run.templates.get(templateId) ?? null\n}\n\nasync function runEmailStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n input: {\n enrollment: OutreachEnrollment\n sequence: OutreachSequence\n person: OutreachEnrollCandidate | null\n lookups: OutreachGateLookups | null\n siteName: string\n },\n): Promise<void> {\n const { enrollment, sequence, person } = input\n const { report, mailbox } = run\n const nowMs = deps.now()\n const stop = async (event: OutreachEnrollmentEvent, counter: 'stopped' | 'failed') => {\n const outcome = await applyOutreachEvent(firestore, { orgId: run.orgId, enrollmentId: enrollment.id, event, nowMs })\n if (outcome.changed) report[counter] += 1\n }\n\n // Every gate again, on the person as they stand now.\n if (!person?.contact) {\n return stop(\n {\n type: 'stop',\n atMs: nowMs,\n byUid: null,\n reason: 'gate',\n detail:\n enrollment.target === 'lead'\n ? \"This lead no longer exists in the sequence's site's CRM.\"\n : 'This contact no longer exists in the CRM.',\n },\n 'stopped',\n )\n }\n if (person.convertedContactId && enrollment.target === 'lead') {\n // The lead converted and the enrollment has not followed it yet: the\n // conversion's listener re-points it, and the next run sends as the contact.\n report.held += 1\n return\n }\n if (!person.visible) {\n return stop(\n { type: 'stop', atMs: nowMs, byUid: null, reason: 'gate', detail: \"This contact is no longer in the sequence's site's CRM.\" },\n 'stopped',\n )\n }\n if (uncheckable(input.lookups)) {\n report.held += 1\n return\n }\n const contactGroupId = consentGroupForHost(run.org, enrollment.hostId).groupId\n const gate = evaluateOutreachGates({\n email: enrollment.email,\n contact: person.contact,\n contactGroupId,\n company: person.company,\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n run.settings.allowedCountries,\n ),\n },\n personalLine: enrollment.personalLine,\n attestations: enrollment.attestations,\n enrollmentId: enrollment.id,\n lookups: input.lookups as OutreachGateLookups,\n })\n if (!gate.allowed) return stop(gateEvent(gate.blocks, nowMs), 'stopped')\n\n /*\n * The email, with its way out. The footer's \"reply 'no'\" line and the\n * postal address are always there (compose refuses without them). The\n * `List-Unsubscribe` header is the sequence's own setting (AGL-3296), read\n * here per send like click tracking, and absent reads as off.\n */\n const unsubscribe = outreachListUnsubscribe({\n enabled: sequence.settings.listUnsubscribe,\n mintUrl: () => deps.unsubscribeUrl({ orgId: run.orgId, enrollmentId: enrollment.id }),\n mailboxEmail: mailbox.email,\n })\n if (unsubscribe.status === 'unavailable') {\n // On, but no link can be minted — no secret, or no HTTPS console origin\n // — and an email without the way out its sequence promised does not leave.\n console.error('[outreach] no unsubscribe link could be minted; nothing sends until one can')\n run.stopped = true\n report.held += 1\n return\n }\n const step = sequence.steps[enrollment.stepIndex]\n const sender = senderOf(mailbox)\n /*\n * Click tracking is the sequence's own setting (AGL-3239) and is read\n * here, per send, rather than once per run: a member turning it off\n * applies to the next email, not to the next hour. A link the minter\n * cannot sign is left exactly as the step wrote it.\n */\n const rewriteLink = sequence.settings.trackClicks\n ? (link: { url: string; index: number }) =>\n deps.clickUrl({\n orgId: run.orgId,\n enrollmentId: enrollment.id,\n stepIndex: enrollment.stepIndex,\n linkIndex: link.index,\n url: link.url,\n })\n : null\n const composed = composeOutreachEmail({\n sequence,\n enrollment,\n orgSettings: run.settings,\n rewriteLink,\n merge: {\n // For a lead, the contact-shaped view of it (`leadAsContact`), so a\n // step written with `{{contact.*}}` reads the lead; `{{lead.*}}` reads\n // the lead's own document beside it.\n contact: person.contact,\n contactGroupId,\n lead: person.lead,\n sender: { name: mailbox.displayName, email: sender.address },\n site: { name: input.siteName },\n },\n templateBody: step?.kind === 'email' && step.templateId ? await templateBody(firestore, run, step.templateId) : null,\n listUnsubscribeUrl: unsubscribe.status === 'ready' ? unsubscribe.url : null,\n listUnsubscribeMailto: unsubscribe.status === 'ready' ? unsubscribe.mailto : null,\n })\n if (composed.error || !composed.email) {\n const code = composed.error?.code\n if (code === 'missing_postal_address' || code === 'missing_legal_name') {\n // The organization's footer: nothing of theirs sends until it is set.\n run.stopped = true\n report.held += 1\n return\n }\n if (code === 'invalid_recipient' || code === 'missing_thread' || code === 'not_an_email_step') {\n return stop({ type: 'send_failed', atMs: nowMs, detail: composed.error?.message }, 'failed')\n }\n // The sequence's own words: the rep fixes the step and resumes.\n return stop({ type: 'pause', atMs: nowMs, byUid: null, detail: composed.error?.message }, 'failed')\n }\n if (composed.unresolvedFields.length) {\n const fields = composed.unresolvedFields.map((field) => `{{${field}}}`).join(', ')\n return stop(\n {\n type: 'pause',\n atMs: nowMs,\n byUid: null,\n detail: `Paused before sending: this contact has nothing for ${fields}. Fill it in and resume.`,\n },\n 'failed',\n )\n }\n\n // Claimed, then sent, then recorded.\n const domain = sender.address.slice(sender.address.lastIndexOf('@') + 1)\n const local = randomUUID()\n const messageId = `<${local}@${domain}>`\n const claimed = await claimStep(deps, firestore, run, enrollment, messageId)\n if (claimed !== 'claimed') {\n if (claimed === 'cap') run.stopped = true\n report.held += 1\n return\n }\n const client = await openClient(deps, run)\n if (!client) {\n await releaseClaim(deps, firestore, run, enrollment, true)\n report.held += 1\n return\n }\n let sent: Awaited<ReturnType<typeof sendComposedOutreachEmail>>\n try {\n sent = await sendComposedOutreachEmail(client, composed.email, sender, {\n messageIdLocalPart: local,\n messageIdDomain: domain,\n })\n } catch (error) {\n if (error instanceof Rfc5322MessageError || (error instanceof GmailTransportError && error.code === 'invalid_request')) {\n await releaseClaim(deps, firestore, run, enrollment, true, {\n type: 'send_failed',\n atMs: deps.now(),\n detail: `Gmail refused the email: ${error.message}`,\n })\n report.failed += 1\n return\n }\n await releaseClaim(deps, firestore, run, enrollment, true)\n report.held += 1\n run.stopped = true\n const code = error instanceof GmailTransportError ? error.code : 'unexpected'\n if (isReconnectRequired(error)) {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId: run.orgId,\n mailboxId: mailbox.id,\n errorCode: code,\n nowMs: deps.now(),\n })\n } else {\n await mailboxRef(firestore, run.orgId, mailbox.id)\n .update({ 'health.lastErrorAtMs': deps.now(), 'health.lastErrorCode': code })\n .catch(() => undefined)\n }\n console.error(`[outreach] a send from a mailbox failed (${code}); its sends wait for the next run`)\n return\n }\n\n const record: OutreachStepRecord = {\n stepIndex: enrollment.stepIndex,\n stepId: step?.id ?? '',\n kind: 'email',\n atMs: deps.now(),\n gmailMessageId: sent.gmailMessageId,\n gmailThreadId: sent.threadId,\n messageId: sent.messageId,\n subject: sent.subject,\n ...(composed.email.trackedLinks.length ? { links: composed.email.trackedLinks } : {}),\n }\n await completeStep(deps, firestore, run, {\n enrollment,\n sequence,\n record,\n sent: { messageId: sent.messageId, threadId: sent.threadId, subject: sent.subject },\n claimMessageId: messageId,\n })\n report.sent += 1\n await fileOutreachEmail(deps, {\n orgId: run.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n direction: 'outbound',\n subject: sent.subject,\n from: sender.address,\n to: composed.email.to,\n messageId: sent.messageId,\n inReplyTo: composed.email.inReplyTo ?? null,\n body: composed.email.text,\n atMs: record.atMs,\n byUid: mailbox.connectedByUid,\n byName: mailbox.displayName || null,\n })\n // The first email is the touch (AGL-3254): `sent` on the sequence's\n // campaigns, and the person's record credited to the first of them.\n // Judged on the enrollment as it was read, before this step's record.\n await creditOutreachFirstSend(deps, { enrollment, atMs: record.atMs })\n}\n\n/**\n * Settles a claim a run died holding. A task step's claim is dropped and\n * the step runs again (its task is filed once however often it is asked).\n * An email's claim looks for the message it minted in the mailbox: found,\n * the send happened and is recorded; not found, it did not, and the claim\n * and its reservation are handed back for the next run.\n */\nasync function recoverClaim(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n enrollment: OutreachEnrollment,\n sequence: OutreachSequence,\n): Promise<boolean> {\n const claim = enrollment.sendClaim\n if (!claim) return false\n if (!claim.messageId) {\n await releaseClaim(deps, firestore, run, enrollment, false)\n return true\n }\n const client = await openClient(deps, run)\n if (!client) return false\n const bare = claim.messageId.replace(/^<|>$/g, '')\n const found = await client.listMessages({ q: `rfc822msgid:${bare}`, maxResults: 1, includeSpamTrash: true })\n const hit = found.messages[0]\n if (!hit) {\n await releaseClaim(deps, firestore, run, enrollment, true)\n return true\n }\n const message = await client.getMessage(hit.id, { metadataHeaders: ['Subject'] })\n const subject = message.headers.find((header) => header.name.toLowerCase() === 'subject')?.value ?? ''\n const step = sequence.steps[enrollment.stepIndex]\n const atMs = deps.now()\n const completed = await completeStep(deps, firestore, run, {\n enrollment,\n sequence,\n record: {\n stepIndex: enrollment.stepIndex,\n stepId: step?.id ?? '',\n kind: 'email',\n atMs,\n gmailMessageId: hit.id,\n gmailThreadId: hit.threadId,\n messageId: claim.messageId,\n subject,\n },\n sent: { messageId: claim.messageId, threadId: hit.threadId, subject },\n claimMessageId: claim.messageId,\n })\n // A recovered first email is still the first email (AGL-3254).\n if (completed) await creditOutreachFirstSend(deps, { enrollment, atMs })\n return completed\n}\n"],"names":["consentGroupForHost","CRM_COLLECTIONS","normalizeCrmEmailTemplate","FieldValue","randomUUID","composeOutreachEmail","planOutreachStepCompletion","evaluateOutreachGates","outreachSentToday","outreachTickAllowance","selectDueOutreachEnrollments","readOutreachEnrollCandidates","readOutreachGateLookups","mailboxRef","outreachEffectiveDailyCap","outreachLocalDay","effectiveOutreachAllowedCountries","OUTREACH_COLLECTIONS","readOutreachComplianceSettingsDoc","outreachEnrollmentLink","outreachEnrollmentPerson","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","Rfc5322MessageError","sendComposedOutreachEmail","creditOutreachFirstSend","applyOutreachEvent","outreachSendDigest","withRecentSend","noteOutreachMailboxReconnectRequired","fileOutreachEmail","fileOutreachTask","OUTREACH_TASK_KIND_TO_RECORD","outreachListUnsubscribe","OUTREACH_SEND_JOB_ID","OUTREACH_SEND_SCAN_LIMIT","OUTREACH_CLAIM_STALE_MS","blankReport","due","sent","tasks","stopped","failed","held","recovered","runOutreachSendJob","deps","context","firestore","report","collectionGroup","enrollments","where","nowMs","orderBy","limit","get","size","byOrg","Map","doc","docs","mailboxes","orgId","ref","parent","id","enrollment","data","set","mailboxId","waiting","values","reduce","sum","list","length","now","deadlineMs","orgSnapshot","collection","org","exists","orgRefusal","settings","runMailbox","unusableMailbox","mailbox","status","member","connectedByUid","input","snapshot","undefined","unusable","outcome","enrollmentId","event","type","atMs","byUid","reason","detail","changed","sendRefusal","uid","run","opened","templates","sequenceIds","Set","map","sequenceId","sequenceSnapshots","getAll","sequences","forEach","sequenceSnapshot","index","sequence","candidates","claim","sendClaim","recoverClaim","push","allowance","selection","deferred","candidate","runTaskStep","emails","people","lookups","byHost","hostId","siteNames","host","contactGroupId","groupId","candidatesRead","lookupsRead","Promise","all","person","personId","contactId","kind","leadId","email","answer","String","runEmailStep","siteName","uncheckable","Object","some","value","gateEvent","blocks","optedOut","find","block","code","join","senderOf","address","sendAs","name","displayName","openClient","openMailbox","ok","client","errorCode","claimStep","messageId","enrollmentRef","boxRef","runTransaction","transaction","current","box","stored","stepIndex","nextDueAtMs","health","timezone","update","token","updatedAtMs","releaseClaim","reserved","extra","sentOnDay","Math","max","Number","sentToday","completeStep","firstEmail","completed","plan","resolve","claimMessageId","stepRecords","record","completedAtMs","random","patch","slice","day","counts","daily","bounces","replies","recentSends","bounced","bumpOutreachSequenceStats","trackedLinks","links","increment","catch","error","console","warn","step","steps","taskId","written","link","dedupeKey","title","notes","taskKind","dueAtMs","assigneeUid","stepId","templateBody","templateId","has","emailTemplates","body","composed","stop","counter","contact","target","convertedContactId","visible","gate","company","allowedCountries","personalLine","attestations","allowed","unsubscribe","enabled","listUnsubscribe","mintUrl","unsubscribeUrl","mailboxEmail","sender","rewriteLink","trackClicks","clickUrl","linkIndex","url","orgSettings","merge","lead","site","listUnsubscribeUrl","listUnsubscribeMailto","mailto","message","unresolvedFields","fields","field","domain","lastIndexOf","local","claimed","messageIdLocalPart","messageIdDomain","gmailMessageId","gmailThreadId","threadId","subject","direction","from","to","inReplyTo","text","byName","bare","replace","found","listMessages","q","maxResults","includeSpamTrash","hit","messages","getMessage","metadataHeaders","headers","header","toLowerCase"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/send-job.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { normalizeCrmEmailTemplate } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { randomUUID } from 'node:crypto'\nimport { composeOutreachEmail } from '../engine/compose'\nimport { planOutreachStepCompletion, type OutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport { evaluateOutreachGates, type OutreachGateBlock, type OutreachGateLookups } from '../engine/gates'\nimport {\n outreachSentToday,\n outreachTickAllowance,\n selectDueOutreachEnrollments,\n type OutreachDueCandidate,\n} from '../engine/sending-capacity'\nimport { readOutreachEnrollCandidates, type OutreachEnrollCandidate } from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport { outreachEffectiveDailyCap, outreachLocalDay } from '../mailboxes/mailbox-settings'\nimport type { OpenedOutreachMailbox } from '../mailboxes/mailbox-transport'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachComplianceSettingsDocument,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachStepRecord,\n type OutreachTaskStep,\n} from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachEnrollmentLink,\n outreachEnrollmentPerson,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { Rfc5322MessageError } from '../transport/rfc5322'\nimport { sendComposedOutreachEmail } from '../transport/send-message'\nimport { creditOutreachFirstSend } from './campaign-credit'\nimport { newOutreachLinkId, outreachStoredLink, type OutreachStoredLink } from './click-link'\nimport { applyOutreachEvent } from './enrollment-events'\nimport { outreachSendDigest, withRecentSend } from './mailbox-health-store'\nimport { noteOutreachMailboxReconnectRequired } from './mailbox-notices'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { fileOutreachEmail, fileOutreachTask, OUTREACH_TASK_KIND_TO_RECORD } from './timeline'\nimport { outreachListUnsubscribe } from './unsubscribe-link'\n\n/*==========================================\n * THE SEND JOB (AGL-2981): every fifteen minutes, on the console.\n *\n * ## What comes due\n *\n * One collection-group read finds every active enrollment whose next step\n * is due, oldest first, bounded per run. They are grouped by workspace and\n * mailbox, and each group is judged before anything is sent:\n *\n * - the workspace: Outreach switched on, entitled and released to it;\n * - the mailbox: an enrollment whose mailbox is gone, disconnected, or whose\n * member left the organization is STOPPED — nothing will ever send it, and\n * leaving it active would show a sequence that is not running. A paused\n * mailbox, or one waiting to be reconnected, holds its enrollments;\n * - the member it sends as: a lockdown of the platform, the workspace or the\n * member holds everything.\n *\n * ## How much goes\n *\n * The engine's per-run allowance — the day's cap and ramp, spread across\n * the window, at most five a run — picks the enrollments (follow-ups first);\n * a task step is due whenever its time comes and spends no allowance.\n *\n * ## One send, exactly once\n *\n * Each email is CLAIMED before it is sent: a transaction over the enrollment\n * and its mailbox checks the enrollment is still active on the same step\n * with no live claim, and that the mailbox's day has room under its cap,\n * then writes the claim and reserves the day's count. Two runs racing for\n * one enrollment write one claim; two racing for one mailbox's last slot\n * reserve it once. The claim carries the `Message-ID` minted for the send,\n * so a claim a run died holding is settled by finding the message in the\n * mailbox rather than by sending it a second time. A send that fails hands\n * its reservation back.\n *\n * Before it is claimed, every gate is asked again — the do-not-contact\n * list, the suppression lists, the sales topic, the roster, the other\n * sequences — because a person enrolled on Monday can unsubscribe by\n * Thursday. A lookup that could not be made holds the send; a gate that\n * refuses stops the enrollment, saying why.\n *==========================================*/\n\nexport { OUTREACH_SEND_JOB_ID } from '../constants/runtime-jobs'\n\n/** The most due enrollments one run reads. */\nexport const OUTREACH_SEND_SCAN_LIMIT = 300\n\n/** How long a claim holds before a later run settles it. */\nexport const OUTREACH_CLAIM_STALE_MS = 10 * 60_000\n\n/** What one run did, for the tick's answer. */\nexport interface OutreachSendReport {\n [key: string]: number\n due: number\n sent: number\n tasks: number\n /** Stopped by a gate or an unusable mailbox. */\n stopped: number\n /** Paused for the rep to fix, or failed for good. */\n failed: number\n /** Due, and left for a later run: the allowance, a lookup, a hold. */\n held: number\n /** Claims a dead run left, settled. */\n recovered: number\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\ninterface MailboxRun {\n orgId: string\n org: Record<string, unknown>\n mailbox: OutreachMailbox\n settings: OutreachComplianceSettingsDocument\n report: OutreachSendReport\n deadlineMs: number\n opened: OpenedOutreachMailbox | null\n /** Set once the mailbox itself cannot send this run. */\n stopped: boolean\n templates: Map<string, string | null>\n}\n\nconst blankReport = (): OutreachSendReport => ({\n due: 0,\n sent: 0,\n tasks: 0,\n stopped: 0,\n failed: 0,\n held: 0,\n recovered: 0,\n})\n\n/** Runs the send job once — see the module note. */\nexport async function runOutreachSendJob(\n deps: OutreachRuntimeDeps,\n context: { nowMs: number; deadlineMs: number },\n): Promise<OutreachSendReport> {\n const firestore = deps.firestore()\n const report = blankReport()\n const due = await firestore\n .collectionGroup(OUTREACH_COLLECTIONS.enrollments)\n .where('status', '==', 'active')\n .where('nextDueAtMs', '<=', context.nowMs)\n .orderBy('nextDueAtMs')\n .limit(OUTREACH_SEND_SCAN_LIMIT)\n .get()\n report.due = due.size\n const byOrg = new Map<string, Map<string, OutreachEnrollment[]>>()\n for (const doc of due.docs) {\n const orgId = doc.ref.parent.parent?.id\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!orgId || !enrollment) continue\n const mailboxes = byOrg.get(orgId) ?? new Map<string, OutreachEnrollment[]>()\n mailboxes.set(enrollment.mailboxId, [...(mailboxes.get(enrollment.mailboxId) ?? []), enrollment])\n byOrg.set(orgId, mailboxes)\n }\n\n for (const [orgId, mailboxes] of byOrg) {\n const waiting = [...mailboxes.values()].reduce((sum, list) => sum + list.length, 0)\n if (deps.now() >= context.deadlineMs) {\n report.held += waiting\n continue\n }\n const orgSnapshot = await firestore.collection('orgs').doc(orgId).get()\n const org = (orgSnapshot.exists ? orgSnapshot.data() : null) as Record<string, unknown> | null\n if (!org || (await deps.orgRefusal(orgId, org))) {\n report.held += waiting\n continue\n }\n const settings = await readOutreachComplianceSettingsDoc(firestore, orgId)\n for (const [mailboxId, enrollments] of mailboxes) {\n await runMailbox(deps, firestore, { orgId, org, mailboxId, enrollments, settings, report, context })\n }\n }\n return report\n}\n\n/** The reason an unusable mailbox stops an enrollment, or `null` for a usable one. */\nasync function unusableMailbox(\n firestore: Firestore,\n orgId: string,\n mailbox: OutreachMailbox | null,\n): Promise<string | null> {\n if (!mailbox) return 'The mailbox this sequence sends from was removed.'\n if (mailbox.status === 'disconnected') return 'The mailbox this sequence sends from was disconnected.'\n const member = await firestore.collection('orgs').doc(orgId).collection('members').doc(mailbox.connectedByUid).get()\n return member.exists\n ? null\n : 'The member who connected the mailbox this sequence sends from is no longer in the organization.'\n}\n\nasync function runMailbox(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n input: {\n orgId: string\n org: Record<string, unknown>\n mailboxId: string\n enrollments: OutreachEnrollment[]\n settings: OutreachComplianceSettingsDocument\n report: OutreachSendReport\n context: { nowMs: number; deadlineMs: number }\n },\n): Promise<void> {\n const { orgId, org, report, context } = input\n const nowMs = context.nowMs\n const snapshot = await mailboxRef(firestore, orgId, input.mailboxId).get()\n const mailbox = readStoredOutreachMailbox(input.mailboxId, snapshot.exists ? snapshot.data() : undefined)\n const unusable = await unusableMailbox(firestore, orgId, mailbox)\n if (unusable || !mailbox) {\n for (const enrollment of input.enrollments) {\n const outcome = await applyOutreachEvent(firestore, {\n orgId,\n enrollmentId: enrollment.id,\n event: { type: 'stop', atMs: nowMs, byUid: null, reason: 'gate', detail: unusable },\n nowMs,\n })\n if (outcome.changed) report.stopped += 1\n }\n return\n }\n if (mailbox.status !== 'connected' || (await deps.sendRefusal({ orgId, org, uid: mailbox.connectedByUid }))) {\n report.held += input.enrollments.length\n return\n }\n\n const run: MailboxRun = {\n orgId,\n org,\n mailbox,\n settings: input.settings,\n report,\n deadlineMs: context.deadlineMs,\n opened: null,\n stopped: false,\n templates: new Map(),\n }\n const sequenceIds = [...new Set(input.enrollments.map((enrollment) => enrollment.sequenceId))]\n const sequenceSnapshots = await firestore.getAll(\n ...sequenceIds.map((id) => outreachOrgCollection(firestore, orgId, 'sequences').doc(id)),\n )\n const sequences = new Map<string, OutreachSequence>()\n sequenceSnapshots.forEach((sequenceSnapshot, index) => {\n const sequence = readStoredOutreachSequence(\n sequenceIds[index],\n sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined,\n )\n if (sequence) sequences.set(sequence.id, sequence)\n })\n\n const candidates: OutreachDueCandidate[] = []\n for (const enrollment of input.enrollments) {\n const sequence = sequences.get(enrollment.sequenceId)\n const claim = enrollment.sendClaim\n if (claim && nowMs - claim.atMs < OUTREACH_CLAIM_STALE_MS) continue\n if (claim && sequence) {\n if (await recoverClaim(deps, firestore, run, enrollment, sequence)) report.recovered += 1\n continue\n }\n if (sequence) candidates.push({ enrollment, sequence })\n }\n\n const allowance = outreachTickAllowance({ mailbox, nowMs })\n const selection = selectDueOutreachEnrollments({ candidates, mailbox, nowMs, allowance: allowance.allowance })\n report.held += selection.deferred.length\n\n for (const candidate of selection.tasks) {\n if (deps.now() >= context.deadlineMs) {\n report.held += 1\n continue\n }\n if (await runTaskStep(deps, firestore, run, candidate)) report.tasks += 1\n }\n if (!selection.emails.length) return\n\n // The people behind the emails, read once per site: the contact and its\n // company, and every lookup the gates decide on.\n const people = new Map<string, OutreachEnrollCandidate>()\n const lookups = new Map<string, OutreachGateLookups>()\n const byHost = new Map<string, OutreachEnrollment[]>()\n for (const candidate of selection.emails) {\n const enrollment = candidate.enrollment as OutreachEnrollment\n byHost.set(enrollment.hostId, [...(byHost.get(enrollment.hostId) ?? []), enrollment])\n }\n const siteNames = new Map<string, string>()\n for (const [hostId, enrollments] of byHost) {\n const contactGroupId = consentGroupForHost(org, hostId).groupId\n // Each by the record it names — the contact, or the lead while the\n // person is one (AGL-3234) — keyed by that record's id.\n const [candidatesRead, lookupsRead, host] = await Promise.all([\n readOutreachEnrollCandidates(firestore, {\n orgId,\n hostId,\n contactGroupId,\n people: enrollments.map(outreachEnrollmentPerson),\n }),\n readOutreachGateLookups(firestore, {\n orgId,\n hostId,\n people: enrollments.map((enrollment) => {\n const person = outreachEnrollmentPerson(enrollment)\n return {\n personId: person.id,\n contactId: person.kind === 'contact' ? person.id : null,\n leadId: person.kind === 'lead' ? person.id : null,\n email: enrollment.email,\n }\n }),\n }),\n firestore.collection('hosts').doc(hostId).get(),\n ])\n for (const person of candidatesRead) people.set(person.personId, person)\n for (const [personId, answer] of lookupsRead) lookups.set(personId, answer)\n siteNames.set(hostId, host.exists ? String(host.get('name') ?? '') : '')\n }\n\n for (const candidate of selection.emails) {\n if (run.stopped || deps.now() >= context.deadlineMs) {\n report.held += 1\n continue\n }\n const enrollment = candidate.enrollment as OutreachEnrollment\n const personId = outreachEnrollmentPerson(enrollment).id\n await runEmailStep(deps, firestore, run, {\n enrollment,\n sequence: candidate.sequence as OutreachSequence,\n person: people.get(personId) ?? null,\n lookups: lookups.get(personId) ?? null,\n siteName: siteNames.get(enrollment.hostId) ?? '',\n })\n }\n}\n\n/** Any lookup the runtime could not make: the send waits for a run that can. */\nfunction uncheckable(lookups: OutreachGateLookups | null): boolean {\n return !lookups || Object.values(lookups).some((value) => value === null)\n}\n\n/** The event a gate's refusal ends an enrollment with. */\nfunction gateEvent(blocks: readonly OutreachGateBlock[], atMs: number): OutreachEnrollmentEvent {\n const optedOut = blocks.find((block) => block.code === 'do_not_contact' || block.code === 'sales_opted_out')\n if (optedOut) return { type: 'opt_out', atMs, reason: 'do_not_contact', detail: optedOut.reason }\n return { type: 'stop', atMs, byUid: null, reason: 'gate', detail: blocks.map((block) => block.reason).join(' ') }\n}\n\n/** The sender line a mailbox's mail goes out with. */\nconst senderOf = (mailbox: OutreachMailbox) => ({\n address: mailbox.sendAs || mailbox.email,\n name: mailbox.displayName || null,\n})\n\nasync function openClient(deps: OutreachRuntimeDeps, run: MailboxRun) {\n const opened = (run.opened ??= await deps.openMailbox(run.mailbox.id))\n if (opened.ok === true) return opened.client\n run.stopped = true\n if (opened.reason !== 'not-configured') {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId: run.orgId,\n mailboxId: run.mailbox.id,\n errorCode: opened.reason,\n nowMs: deps.now(),\n })\n }\n return null\n}\n\n/**\n * Claims one step: the enrollment still active on the same step with no\n * live claim, and — for an email — a slot under the mailbox's cap today,\n * reserved in the same transaction.\n */\nasync function claimStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n enrollment: OutreachEnrollment,\n messageId: string | null,\n): Promise<'claimed' | 'taken' | 'cap'> {\n const enrollmentRef = outreachOrgCollection(firestore, run.orgId, 'enrollments').doc(enrollment.id)\n const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id)\n const nowMs = deps.now()\n return firestore.runTransaction(async (transaction) => {\n const [current, box] = await Promise.all([transaction.get(enrollmentRef), transaction.get(boxRef)])\n const stored = readStoredOutreachEnrollment(enrollment.id, current.exists ? current.data() : undefined)\n const claim = stored?.sendClaim\n if (\n !stored ||\n stored.status !== 'active' ||\n stored.stepIndex !== enrollment.stepIndex ||\n stored.nextDueAtMs === null ||\n stored.nextDueAtMs > nowMs ||\n (claim && nowMs - claim.atMs < OUTREACH_CLAIM_STALE_MS)\n ) {\n return 'taken'\n }\n if (messageId) {\n const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.exists ? box.data() : undefined)\n if (!mailbox || mailbox.status !== 'connected') return 'taken'\n const sent = outreachSentToday(mailbox.health, nowMs, mailbox.timezone)\n if (sent >= outreachEffectiveDailyCap(mailbox, nowMs)) return 'cap'\n transaction.update(boxRef, {\n 'health.sentToday': sent + 1,\n 'health.sentOnDay': outreachLocalDay(nowMs, mailbox.timezone),\n })\n }\n transaction.update(enrollmentRef, {\n sendClaim: { token: randomUUID(), atMs: nowMs, stepIndex: enrollment.stepIndex, messageId },\n updatedAtMs: nowMs,\n })\n return 'claimed'\n })\n}\n\n/** Drops a claim, and hands an email's reservation back to the mailbox's day. */\nasync function releaseClaim(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n enrollment: Pick<OutreachEnrollment, 'id'>,\n reserved: boolean,\n event: OutreachEnrollmentEvent | null = null,\n): Promise<void> {\n const nowMs = deps.now()\n await applyOutreachEvent(firestore, {\n orgId: run.orgId,\n enrollmentId: enrollment.id,\n event,\n extra: () => ({ sendClaim: null }),\n nowMs,\n })\n if (!reserved) return\n const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id)\n await firestore.runTransaction(async (transaction) => {\n const box = await transaction.get(boxRef)\n const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.exists ? box.data() : undefined)\n if (!mailbox || mailbox.health?.sentOnDay !== outreachLocalDay(nowMs, mailbox.timezone)) return\n transaction.update(boxRef, { 'health.sentToday': Math.max(0, (Number(mailbox.health.sentToday) || 0) - 1) })\n })\n}\n\n/**\n * Records a step that ran: the engine's completion patch, the claim\n * cleared, the step's record kept — and for an email the mailbox's send on\n * its day, its last-sent time and its window of recent sends. Refuses\n * (answers false) when the claim is no longer this run's.\n */\nasync function completeStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n input: {\n enrollment: OutreachEnrollment\n sequence: OutreachSequence\n record: OutreachStepRecord\n sent: { messageId: string; threadId: string; subject: string } | null\n claimMessageId: string | null\n },\n): Promise<boolean> {\n const enrollmentRef = outreachOrgCollection(firestore, run.orgId, 'enrollments').doc(input.enrollment.id)\n const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id)\n const nowMs = deps.now()\n /*\n * Whether this is the FIRST email this person has had from the sequence\n * (AGL-3239), decided from the same read the step is recorded against and\n * so re-decided on every transaction retry. It is what `stats.people`\n * counts: the denominator of every engagement rate, and the thing\n * `stats.sent` is not, since one person takes several steps.\n */\n let firstEmail = false\n const completed = await firestore.runTransaction(async (transaction) => {\n const [current, box] = await Promise.all([\n transaction.get(enrollmentRef),\n input.sent ? transaction.get(boxRef) : Promise.resolve(null),\n ])\n const stored = readStoredOutreachEnrollment(input.enrollment.id, current.exists ? current.data() : undefined)\n if (!stored?.sendClaim || stored.sendClaim.messageId !== input.claimMessageId) return false\n if (stored.stepIndex !== input.enrollment.stepIndex) return false\n // The step RAN, whatever happened to the enrollment while it did — a\n // member paused it, a reply stopped it — so it is recorded as run, and a\n // resume never sends it twice.\n firstEmail =\n input.sent !== null &&\n !(stored.stepRecords ?? []).some((record) => record.kind === 'email')\n const plan = planOutreachStepCompletion({\n enrollment: { ...stored, status: 'active' },\n sequence: input.sequence,\n mailbox: run.mailbox,\n completedAtMs: nowMs,\n sent: input.sent,\n random: deps.random,\n })\n const patch: Record<string, unknown> = {\n sendClaim: null,\n stepRecords: [...(stored.stepRecords ?? []), input.record].slice(-20),\n updatedAtMs: nowMs,\n ...(plan.patch ?? {}),\n }\n if (stored.status !== 'active') {\n patch['status'] = stored.status\n if (stored.status !== 'paused') patch['nextDueAtMs'] = null\n }\n transaction.update(enrollmentRef, patch)\n if (input.sent && box?.exists) {\n const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.data())\n if (mailbox) {\n const day = outreachLocalDay(nowMs, mailbox.timezone)\n const counts = mailbox.health?.daily?.[day] ?? { sent: 0, bounces: 0, replies: 0 }\n transaction.update(boxRef, {\n 'health.lastSentAtMs': nowMs,\n 'health.lastErrorAtMs': null,\n 'health.lastErrorCode': null,\n [`health.daily.${day}`]: { ...counts, sent: (Number(counts.sent) || 0) + 1 },\n 'health.recentSends': withRecentSend(mailbox.health?.recentSends, {\n id: outreachSendDigest(input.sent.messageId),\n atMs: nowMs,\n bounced: false,\n }),\n updatedAtMs: nowMs,\n })\n }\n }\n return true\n })\n if (completed && input.sent) {\n await bumpOutreachSequenceStats(firestore, {\n orgId: run.orgId,\n sequenceId: input.sequence.id,\n firstEmail,\n trackedLinks: input.record.links?.length ?? 0,\n })\n }\n return completed\n}\n\n/**\n * The sequence's send counters (AGL-3239), written outside the enrollment's\n * transaction.\n *\n * One sequence holds every enrollment, so a transaction over it would\n * serialise every recipient against every other; `FieldValue.increment` is a\n * blind write with nothing to contend on. It is bookkeeping beside the act,\n * so a failure is logged and the send stands — the email has already left.\n *\n * `clickTracked` is stamped by the first send that actually carried a\n * rewritten link, never by the SETTING: a sequence with the setting on whose\n * emails have no links in them measured nothing, and the report must say\n * \"not measured\" for it rather than publish a 0% click rate.\n */\nasync function bumpOutreachSequenceStats(\n firestore: Firestore,\n input: { orgId: string; sequenceId: string; firstEmail: boolean; trackedLinks: number },\n): Promise<void> {\n if (!input.sequenceId) return\n await outreachOrgCollection(firestore, input.orgId, 'sequences')\n .doc(input.sequenceId)\n .update({\n 'stats.sent': FieldValue.increment(1),\n ...(input.firstEmail ? { 'stats.people': FieldValue.increment(1) } : {}),\n ...(input.trackedLinks > 0 ? { 'stats.clickTracked': true } : {}),\n })\n .catch((error: unknown) => {\n console.warn('[outreach] a send could not be counted on its sequence', error)\n })\n}\n\n/** One task step: the rep's task on the contact, then the step recorded. */\nasync function runTaskStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n candidate: OutreachDueCandidate,\n): Promise<boolean> {\n const enrollment = candidate.enrollment as OutreachEnrollment\n const sequence = candidate.sequence as OutreachSequence\n const step = sequence.steps[enrollment.stepIndex] as OutreachTaskStep\n if ((await claimStep(deps, firestore, run, enrollment, null)) !== 'claimed') return false\n const nowMs = deps.now()\n let taskId: string | null\n try {\n const written = await fileOutreachTask(deps, {\n orgId: run.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: `step:${enrollment.id}:${step.id}`,\n title: step.title,\n notes: `From sequence step ${enrollment.stepIndex + 1}.`,\n kind: OUTREACH_TASK_KIND_TO_RECORD[step.taskKind] ?? 'todo',\n dueAtMs: nowMs,\n assigneeUid: run.mailbox.connectedByUid,\n })\n taskId = written?.ok ? written.id : null\n if (written?.ok === false) console.warn(`[outreach] a task step's task was not filed: ${written.error}`)\n } catch (error) {\n // The record system failed rather than refused: the step waits for a\n // run that can file its task.\n console.error('[outreach] filing a task step failed; the step waits', error)\n await releaseClaim(deps, firestore, run, enrollment, false)\n return false\n }\n return completeStep(deps, firestore, run, {\n enrollment,\n sequence,\n record: { stepIndex: enrollment.stepIndex, stepId: step.id, kind: 'task', atMs: nowMs, taskId },\n sent: null,\n claimMessageId: null,\n })\n}\n\n/** A CRM template's body, read once per run. */\nasync function templateBody(firestore: Firestore, run: MailboxRun, templateId: string): Promise<string | null> {\n if (!run.templates.has(templateId)) {\n const snapshot = await firestore\n .collection('orgs')\n .doc(run.orgId)\n .collection(CRM_COLLECTIONS.emailTemplates)\n .doc(templateId)\n .get()\n run.templates.set(\n templateId,\n snapshot.exists ? normalizeCrmEmailTemplate(snapshot.data() as Record<string, unknown>).body : null,\n )\n }\n return run.templates.get(templateId) ?? null\n}\n\nasync function runEmailStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n input: {\n enrollment: OutreachEnrollment\n sequence: OutreachSequence\n person: OutreachEnrollCandidate | null\n lookups: OutreachGateLookups | null\n siteName: string\n },\n): Promise<void> {\n const { enrollment, sequence, person } = input\n const { report, mailbox } = run\n const nowMs = deps.now()\n const stop = async (event: OutreachEnrollmentEvent, counter: 'stopped' | 'failed') => {\n const outcome = await applyOutreachEvent(firestore, { orgId: run.orgId, enrollmentId: enrollment.id, event, nowMs })\n if (outcome.changed) report[counter] += 1\n }\n\n // Every gate again, on the person as they stand now.\n if (!person?.contact) {\n return stop(\n {\n type: 'stop',\n atMs: nowMs,\n byUid: null,\n reason: 'gate',\n detail:\n enrollment.target === 'lead'\n ? \"This lead no longer exists in the sequence's site's CRM.\"\n : 'This contact no longer exists in the CRM.',\n },\n 'stopped',\n )\n }\n if (person.convertedContactId && enrollment.target === 'lead') {\n // The lead converted and the enrollment has not followed it yet: the\n // conversion's listener re-points it, and the next run sends as the contact.\n report.held += 1\n return\n }\n if (!person.visible) {\n return stop(\n { type: 'stop', atMs: nowMs, byUid: null, reason: 'gate', detail: \"This contact is no longer in the sequence's site's CRM.\" },\n 'stopped',\n )\n }\n if (uncheckable(input.lookups)) {\n report.held += 1\n return\n }\n const contactGroupId = consentGroupForHost(run.org, enrollment.hostId).groupId\n const gate = evaluateOutreachGates({\n email: enrollment.email,\n contact: person.contact,\n contactGroupId,\n company: person.company,\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n run.settings.allowedCountries,\n ),\n },\n personalLine: enrollment.personalLine,\n attestations: enrollment.attestations,\n enrollmentId: enrollment.id,\n lookups: input.lookups as OutreachGateLookups,\n })\n if (!gate.allowed) return stop(gateEvent(gate.blocks, nowMs), 'stopped')\n\n /*\n * The email, with its way out. The footer's \"reply 'no'\" line and the\n * postal address are always there (compose refuses without them). The\n * `List-Unsubscribe` header is the sequence's own setting (AGL-3296), read\n * here per send like click tracking, and absent reads as off.\n */\n const unsubscribe = outreachListUnsubscribe({\n enabled: sequence.settings.listUnsubscribe,\n mintUrl: () => deps.unsubscribeUrl({ orgId: run.orgId, enrollmentId: enrollment.id }),\n mailboxEmail: mailbox.email,\n })\n if (unsubscribe.status === 'unavailable') {\n // On, but no link can be minted — no secret, or no HTTPS console origin\n // — and an email without the way out its sequence promised does not leave.\n console.error('[outreach] no unsubscribe link could be minted; nothing sends until one can')\n run.stopped = true\n report.held += 1\n return\n }\n const step = sequence.steps[enrollment.stepIndex]\n const sender = senderOf(mailbox)\n /*\n * Click tracking is the sequence's own setting (AGL-3239) and is read\n * here, per send, rather than once per run: a member turning it off\n * applies to the next email, not to the next hour. Each link becomes a\n * short link (AGL-3297) whose document is written below, before the email\n * leaves; a link that cannot be made is left exactly as the step wrote it.\n */\n const shortLinks: Array<{ id: string; doc: OutreachStoredLink }> = []\n const rewriteLink = sequence.settings.trackClicks\n ? (link: { url: string; index: number }) => {\n const doc = outreachStoredLink(\n {\n orgId: run.orgId,\n enrollmentId: enrollment.id,\n stepIndex: enrollment.stepIndex,\n linkIndex: link.index,\n url: link.url,\n },\n nowMs,\n )\n const id = newOutreachLinkId()\n const url = doc ? deps.clickLinkUrl(id) : null\n if (!doc || !url) return null\n shortLinks.push({ id, doc })\n return url\n }\n : null\n const composed = composeOutreachEmail({\n sequence,\n enrollment,\n orgSettings: run.settings,\n rewriteLink,\n merge: {\n // For a lead, the contact-shaped view of it (`leadAsContact`), so a\n // step written with `{{contact.*}}` reads the lead; `{{lead.*}}` reads\n // the lead's own document beside it.\n contact: person.contact,\n contactGroupId,\n lead: person.lead,\n sender: { name: mailbox.displayName, email: sender.address },\n site: { name: input.siteName },\n },\n templateBody: step?.kind === 'email' && step.templateId ? await templateBody(firestore, run, step.templateId) : null,\n listUnsubscribeUrl: unsubscribe.status === 'ready' ? unsubscribe.url : null,\n listUnsubscribeMailto: unsubscribe.status === 'ready' ? unsubscribe.mailto : null,\n })\n if (composed.error || !composed.email) {\n const code = composed.error?.code\n if (code === 'missing_postal_address' || code === 'missing_legal_name') {\n // The organization's footer: nothing of theirs sends until it is set.\n run.stopped = true\n report.held += 1\n return\n }\n if (code === 'invalid_recipient' || code === 'missing_thread' || code === 'not_an_email_step') {\n return stop({ type: 'send_failed', atMs: nowMs, detail: composed.error?.message }, 'failed')\n }\n // The sequence's own words: the rep fixes the step and resumes.\n return stop({ type: 'pause', atMs: nowMs, byUid: null, detail: composed.error?.message }, 'failed')\n }\n if (composed.unresolvedFields.length) {\n const fields = composed.unresolvedFields.map((field) => `{{${field}}}`).join(', ')\n return stop(\n {\n type: 'pause',\n atMs: nowMs,\n byUid: null,\n detail: `Paused before sending: this contact has nothing for ${fields}. Fill it in and resume.`,\n },\n 'failed',\n )\n }\n\n /*\n * The short links' documents, before anything is claimed or sent: a link\n * in an email that already left must resolve on the first click. A write\n * that fails holds the send for the next run, which mints new ones — an\n * orphaned document is inert, an email whose links go nowhere is not.\n */\n if (shortLinks.length) {\n try {\n const batch = firestore.batch()\n for (const link of shortLinks) {\n batch.create(firestore.collection(OUTREACH_COLLECTIONS.links).doc(link.id), link.doc)\n }\n await batch.commit()\n } catch (error) {\n console.error('[outreach] the short links could not be stored; the send waits for the next run', error)\n report.held += 1\n return\n }\n }\n\n // Claimed, then sent, then recorded.\n const domain = sender.address.slice(sender.address.lastIndexOf('@') + 1)\n const local = randomUUID()\n const messageId = `<${local}@${domain}>`\n const claimed = await claimStep(deps, firestore, run, enrollment, messageId)\n if (claimed !== 'claimed') {\n if (claimed === 'cap') run.stopped = true\n report.held += 1\n return\n }\n const client = await openClient(deps, run)\n if (!client) {\n await releaseClaim(deps, firestore, run, enrollment, true)\n report.held += 1\n return\n }\n let sent: Awaited<ReturnType<typeof sendComposedOutreachEmail>>\n try {\n sent = await sendComposedOutreachEmail(client, composed.email, sender, {\n messageIdLocalPart: local,\n messageIdDomain: domain,\n })\n } catch (error) {\n if (error instanceof Rfc5322MessageError || (error instanceof GmailTransportError && error.code === 'invalid_request')) {\n await releaseClaim(deps, firestore, run, enrollment, true, {\n type: 'send_failed',\n atMs: deps.now(),\n detail: `Gmail refused the email: ${error.message}`,\n })\n report.failed += 1\n return\n }\n await releaseClaim(deps, firestore, run, enrollment, true)\n report.held += 1\n run.stopped = true\n const code = error instanceof GmailTransportError ? error.code : 'unexpected'\n if (isReconnectRequired(error)) {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId: run.orgId,\n mailboxId: mailbox.id,\n errorCode: code,\n nowMs: deps.now(),\n })\n } else {\n await mailboxRef(firestore, run.orgId, mailbox.id)\n .update({ 'health.lastErrorAtMs': deps.now(), 'health.lastErrorCode': code })\n .catch(() => undefined)\n }\n console.error(`[outreach] a send from a mailbox failed (${code}); its sends wait for the next run`)\n return\n }\n\n const record: OutreachStepRecord = {\n stepIndex: enrollment.stepIndex,\n stepId: step?.id ?? '',\n kind: 'email',\n atMs: deps.now(),\n gmailMessageId: sent.gmailMessageId,\n gmailThreadId: sent.threadId,\n messageId: sent.messageId,\n subject: sent.subject,\n ...(composed.email.trackedLinks.length ? { links: composed.email.trackedLinks } : {}),\n }\n await completeStep(deps, firestore, run, {\n enrollment,\n sequence,\n record,\n sent: { messageId: sent.messageId, threadId: sent.threadId, subject: sent.subject },\n claimMessageId: messageId,\n })\n report.sent += 1\n await fileOutreachEmail(deps, {\n orgId: run.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n direction: 'outbound',\n subject: sent.subject,\n from: sender.address,\n to: composed.email.to,\n messageId: sent.messageId,\n inReplyTo: composed.email.inReplyTo ?? null,\n body: composed.email.text,\n atMs: record.atMs,\n byUid: mailbox.connectedByUid,\n byName: mailbox.displayName || null,\n })\n // The first email is the touch (AGL-3254): `sent` on the sequence's\n // campaigns, and the person's record credited to the first of them.\n // Judged on the enrollment as it was read, before this step's record.\n await creditOutreachFirstSend(deps, { enrollment, atMs: record.atMs })\n}\n\n/**\n * Settles a claim a run died holding. A task step's claim is dropped and\n * the step runs again (its task is filed once however often it is asked).\n * An email's claim looks for the message it minted in the mailbox: found,\n * the send happened and is recorded; not found, it did not, and the claim\n * and its reservation are handed back for the next run.\n */\nasync function recoverClaim(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n enrollment: OutreachEnrollment,\n sequence: OutreachSequence,\n): Promise<boolean> {\n const claim = enrollment.sendClaim\n if (!claim) return false\n if (!claim.messageId) {\n await releaseClaim(deps, firestore, run, enrollment, false)\n return true\n }\n const client = await openClient(deps, run)\n if (!client) return false\n const bare = claim.messageId.replace(/^<|>$/g, '')\n const found = await client.listMessages({ q: `rfc822msgid:${bare}`, maxResults: 1, includeSpamTrash: true })\n const hit = found.messages[0]\n if (!hit) {\n await releaseClaim(deps, firestore, run, enrollment, true)\n return true\n }\n const message = await client.getMessage(hit.id, { metadataHeaders: ['Subject'] })\n const subject = message.headers.find((header) => header.name.toLowerCase() === 'subject')?.value ?? ''\n const step = sequence.steps[enrollment.stepIndex]\n const atMs = deps.now()\n const completed = await completeStep(deps, firestore, run, {\n enrollment,\n sequence,\n record: {\n stepIndex: enrollment.stepIndex,\n stepId: step?.id ?? '',\n kind: 'email',\n atMs,\n gmailMessageId: hit.id,\n gmailThreadId: hit.threadId,\n messageId: claim.messageId,\n subject,\n },\n sent: { messageId: claim.messageId, threadId: hit.threadId, subject },\n claimMessageId: claim.messageId,\n })\n // A recovered first email is still the first email (AGL-3254).\n if (completed) await creditOutreachFirstSend(deps, { enrollment, atMs })\n return completed\n}\n"],"names":["consentGroupForHost","CRM_COLLECTIONS","normalizeCrmEmailTemplate","FieldValue","randomUUID","composeOutreachEmail","planOutreachStepCompletion","evaluateOutreachGates","outreachSentToday","outreachTickAllowance","selectDueOutreachEnrollments","readOutreachEnrollCandidates","readOutreachGateLookups","mailboxRef","outreachEffectiveDailyCap","outreachLocalDay","effectiveOutreachAllowedCountries","OUTREACH_COLLECTIONS","readOutreachComplianceSettingsDoc","outreachEnrollmentLink","outreachEnrollmentPerson","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","Rfc5322MessageError","sendComposedOutreachEmail","creditOutreachFirstSend","newOutreachLinkId","outreachStoredLink","applyOutreachEvent","outreachSendDigest","withRecentSend","noteOutreachMailboxReconnectRequired","fileOutreachEmail","fileOutreachTask","OUTREACH_TASK_KIND_TO_RECORD","outreachListUnsubscribe","OUTREACH_SEND_JOB_ID","OUTREACH_SEND_SCAN_LIMIT","OUTREACH_CLAIM_STALE_MS","blankReport","due","sent","tasks","stopped","failed","held","recovered","runOutreachSendJob","deps","context","firestore","report","collectionGroup","enrollments","where","nowMs","orderBy","limit","get","size","byOrg","Map","doc","docs","mailboxes","orgId","ref","parent","id","enrollment","data","set","mailboxId","waiting","values","reduce","sum","list","length","now","deadlineMs","orgSnapshot","collection","org","exists","orgRefusal","settings","runMailbox","unusableMailbox","mailbox","status","member","connectedByUid","input","snapshot","undefined","unusable","outcome","enrollmentId","event","type","atMs","byUid","reason","detail","changed","sendRefusal","uid","run","opened","templates","sequenceIds","Set","map","sequenceId","sequenceSnapshots","getAll","sequences","forEach","sequenceSnapshot","index","sequence","candidates","claim","sendClaim","recoverClaim","push","allowance","selection","deferred","candidate","runTaskStep","emails","people","lookups","byHost","hostId","siteNames","host","contactGroupId","groupId","candidatesRead","lookupsRead","Promise","all","person","personId","contactId","kind","leadId","email","answer","String","runEmailStep","siteName","uncheckable","Object","some","value","gateEvent","blocks","optedOut","find","block","code","join","senderOf","address","sendAs","name","displayName","openClient","openMailbox","ok","client","errorCode","claimStep","messageId","enrollmentRef","boxRef","runTransaction","transaction","current","box","stored","stepIndex","nextDueAtMs","health","timezone","update","token","updatedAtMs","releaseClaim","reserved","extra","sentOnDay","Math","max","Number","sentToday","completeStep","firstEmail","completed","plan","resolve","claimMessageId","stepRecords","record","completedAtMs","random","patch","slice","day","counts","daily","bounces","replies","recentSends","bounced","bumpOutreachSequenceStats","trackedLinks","links","increment","catch","error","console","warn","step","steps","taskId","written","link","dedupeKey","title","notes","taskKind","dueAtMs","assigneeUid","stepId","templateBody","templateId","has","emailTemplates","body","composed","stop","counter","contact","target","convertedContactId","visible","gate","company","allowedCountries","personalLine","attestations","allowed","unsubscribe","enabled","listUnsubscribe","mintUrl","unsubscribeUrl","mailboxEmail","sender","shortLinks","rewriteLink","trackClicks","linkIndex","url","clickLinkUrl","orgSettings","merge","lead","site","listUnsubscribeUrl","listUnsubscribeMailto","mailto","message","unresolvedFields","fields","field","batch","create","commit","domain","lastIndexOf","local","claimed","messageIdLocalPart","messageIdDomain","gmailMessageId","gmailThreadId","threadId","subject","direction","from","to","inReplyTo","text","byName","bare","replace","found","listMessages","q","maxResults","includeSpamTrash","hit","messages","getMessage","metadataHeaders","headers","header","toLowerCase"],"mappings":"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"}
package/src/lib/server.js CHANGED
@@ -83,6 +83,13 @@ import { registerOutreachRoutes } from "./routes/register-routes.js";
83
83
  }, {
84
84
  recipientLink: true
85
85
  });
86
+ // The short tracking link every send carries since AGL-3297, for the same
87
+ // reasons; the signed `click` route above keeps answering older emails.
88
+ registerPluginApiRoute(OUTREACH_API_ROUTES.shortLink, {
89
+ web: async (request, context)=>(await import("./runtime/platform-runtime-deps.js")).platformOutreachShortLinkRoute()(request, context)
90
+ }, {
91
+ recipientLink: true
92
+ });
86
93
  }
87
94
  // The transport the sending runtime reaches a connected mailbox through
88
95
  // (AGL-2978): open a mailbox's Gmail client, mark one reconnect-required,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/outreach/src/lib/server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Outreach's SERVER half (AGL-2974), `@aglyn/plugins-outreach/server`.\n *\n * Every write to an `outreach*` collection is made here: the Firestore rules\n * refuse all of them to clients, so a mailbox, a sequence, an enrollment and a\n * credential only ever change through a handler registered below, behind the\n * session, entitlement and permission checks that handler makes.\n *\n * `outreach/ping` exists so the wiring is proven rather than assumed, as the\n * CRM's does: `plugins.config.json` names `registerOutreachConsoleApi` and the\n * `outreach` prefix, the generated server manifest loads this module, and the\n * console's `/api/[...pluginApi]` dispatcher reaches the handler — after its\n * own gates, which already refuse a request while `release_outreach` is off\n * for the caller. A first real route that also had to be the first wiring\n * test would be two things to debug at once.\n *\n * A route that reads or writes org data resolves the caller the way the CRM's\n * org routes do (`resolveOrgMembership` / `memberHasOrgPermission` from\n * `@aglyn/tenant-data-admin`) and asks `checkEntitlement(org, 'outreach')`\n * and `outreach.use` before it touches a document.\n */\n\nimport {\n registerPluginApiRoute,\n registerPluginPermissions,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { OUTREACH_API_ROUTES } from './constants/api-routes'\nimport {\n OUTREACH_PERMISSIONS,\n OUTREACH_PLUGIN_ID,\n} from './constants/bundle-common'\nimport { registerOutreachMailboxRoutes } from './mailboxes/register-mailbox-routes'\nimport { registerOutreachRoutes } from './routes/register-routes'\n\n/**\n * No auth, no org, no data: it answers whether this server bundle was loaded\n * and its routes registered, which is a fact about the process rather than\n * about any workspace.\n */\nexport const outreachPingHandler: PluginApiHandler = (req, res) => {\n if (req.method !== 'GET') {\n res.setHeader('Allow', 'GET')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n res.status(200).json({ ok: true, plugin: OUTREACH_PLUGIN_ID })\n}\n\n/** Console API registration, named in `plugins.config.json` as `consoleApi`. */\nexport function registerOutreachConsoleApi(): void {\n // The server resolves plugin permission keys from this registry, so a\n // route's `outreach.use` check and the console's gate read the same tier\n // defaults.\n registerPluginPermissions(OUTREACH_PERMISSIONS)\n registerPluginApiRoute(OUTREACH_API_ROUTES.ping, outreachPingHandler)\n // Connect a Google mailbox, its settings, pause, test and disconnect\n // (AGL-2978).\n registerOutreachMailboxRoutes()\n // Compliance settings, sequences, enrollment and enrollment actions\n // (AGL-2980).\n registerOutreachRoutes()\n // The one-click unsubscribe every email carries (AGL-2981): a recipient\n // link, so it answers whether or not Outreach is released to the\n // organization now. Its module loads on the first request.\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.unsubscribe,\n {\n web: async (request, context) =>\n (await import('./runtime/platform-runtime-deps')).platformOutreachUnsubscribeRoute()(request, context),\n },\n { recipientLink: true },\n )\n // A link in a tracked sequence email (AGL-3239): a recipient link for the\n // same reason, and more pressingly — a link already sitting in somebody's\n // inbox has to keep forwarding them whether or not the rollout is paused.\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.click,\n {\n web: async (request, context) =>\n (await import('./runtime/platform-runtime-deps')).platformOutreachClickRoute()(request, context),\n },\n { recipientLink: true },\n )\n}\n\n// The transport the sending runtime reaches a connected mailbox through\n// (AGL-2978): open a mailbox's Gmail client, mark one reconnect-required,\n// send the engine's composed email through the one RFC 5322 writer, and read\n// a thread whole for the engine's reply and bounce classifier.\nexport {\n markOutreachMailboxReconnectRequired,\n openOutreachMailboxClient,\n type OpenedOutreachMailbox,\n} from './mailboxes/mailbox-transport'\nexport {\n GmailTransportError,\n isReconnectRequired,\n type GmailTransportErrorCode,\n} from './transport/gmail-errors'\nexport type {\n GmailClient,\n GmailFullThread,\n GmailMessageMetadata,\n GmailThread,\n} from './transport/gmail-client'\nexport {\n buildRfc5322Message,\n encodeGmailRawMessage,\n LIST_UNSUBSCRIBE_ONE_CLICK,\n Rfc5322MessageError,\n type OutreachComposedMessage,\n type OutreachMailAddress,\n} from './transport/rfc5322'\nexport {\n sendComposedOutreachEmail,\n sendOutreachMessage,\n type OutreachSentMessage,\n type SendOutreachMessageOptions,\n} from './transport/send-message'\n\n// The organization's compliance settings and do-not-contact list\n// (AGL-2980), for the sending runtime: the footer is read before every send,\n// the list is checked before every send, and a reply that opts out, an\n// unsubscribe and a hard bounce each put the address on it.\nexport {\n outreachComplianceSettingsRef,\n readOutreachComplianceSettingsDoc,\n writeOutreachComplianceSettings,\n type WriteOutreachComplianceSettingsInput,\n type WriteOutreachComplianceSettingsResult,\n} from './storage/compliance-settings-store'\nexport {\n addOutreachDoNotContact,\n addOutreachDoNotContactDomain,\n getOutreachDoNotContactEntry,\n isOutreachDoNotContact,\n listOutreachDoNotContactDomains,\n lookupOutreachDoNotContact,\n lookupOutreachDoNotContactDomains,\n outreachDoNotContactCollection,\n outreachDoNotContactDomainCollection,\n readOutreachDoNotContactDomainEntry,\n readOutreachDoNotContactEntry,\n removeOutreachDoNotContactDomain,\n type AddOutreachDoNotContactDomainInput,\n type AddOutreachDoNotContactDomainResult,\n type AddOutreachDoNotContactInput,\n type AddOutreachDoNotContactResult,\n} from './storage/do-not-contact-store'\n"],"names":["registerPluginApiRoute","registerPluginPermissions","OUTREACH_API_ROUTES","OUTREACH_PERMISSIONS","OUTREACH_PLUGIN_ID","registerOutreachMailboxRoutes","registerOutreachRoutes","outreachPingHandler","req","res","method","setHeader","status","json","error","ok","plugin","registerOutreachConsoleApi","ping","unsubscribe","web","request","context","platformOutreachUnsubscribeRoute","recipientLink","click","platformOutreachClickRoute","markOutreachMailboxReconnectRequired","openOutreachMailboxClient","GmailTransportError","isReconnectRequired","buildRfc5322Message","encodeGmailRawMessage","LIST_UNSUBSCRIBE_ONE_CLICK","Rfc5322MessageError","sendComposedOutreachEmail","sendOutreachMessage","outreachComplianceSettingsRef","readOutreachComplianceSettingsDoc","writeOutreachComplianceSettings","addOutreachDoNotContact","addOutreachDoNotContactDomain","getOutreachDoNotContactEntry","isOutreachDoNotContact","listOutreachDoNotContactDomains","lookupOutreachDoNotContact","lookupOutreachDoNotContactDomains","outreachDoNotContactCollection","outreachDoNotContactDomainCollection","readOutreachDoNotContactDomainEntry","readOutreachDoNotContactEntry","removeOutreachDoNotContactDomain"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;CAoBC,GAED,SACEA,sBAAsB,EACtBC,yBAAyB,QAEpB,sBAAqB;AAC5B,SAASC,mBAAmB,QAAQ,4BAAwB;AAC5D,SACEC,oBAAoB,EACpBC,kBAAkB,QACb,+BAA2B;AAClC,SAASC,6BAA6B,QAAQ,yCAAqC;AACnF,SAASC,sBAAsB,QAAQ,8BAA0B;AAEjE;;;;CAIC,GACD,OAAO,MAAMC,sBAAwC,CAACC,KAAKC;IACzD,IAAID,IAAIE,MAAM,KAAK,OAAO;QACxBD,IAAIE,SAAS,CAAC,SAAS;QACvBF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;QACnD;IACF;IACAL,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEE,IAAI;QAAMC,QAAQZ;IAAmB;AAC9D,EAAC;AAED,8EAA8E,GAC9E,OAAO,SAASa;IACd,sEAAsE;IACtE,yEAAyE;IACzE,YAAY;IACZhB,0BAA0BE;IAC1BH,uBAAuBE,oBAAoBgB,IAAI,EAAEX;IACjD,qEAAqE;IACrE,cAAc;IACdF;IACA,oEAAoE;IACpE,cAAc;IACdC;IACA,wEAAwE;IACxE,iEAAiE;IACjE,2DAA2D;IAC3DN,uBACEE,oBAAoBiB,WAAW,EAC/B;QACEC,KAAK,OAAOC,SAASC,UACnB,AAAC,CAAA,MAAM,MAAM,CAAC,qCAAiC,EAAGC,gCAAgC,GAAGF,SAASC;IAClG,GACA;QAAEE,eAAe;IAAK;IAExB,0EAA0E;IAC1E,0EAA0E;IAC1E,0EAA0E;IAC1ExB,uBACEE,oBAAoBuB,KAAK,EACzB;QACEL,KAAK,OAAOC,SAASC,UACnB,AAAC,CAAA,MAAM,MAAM,CAAC,qCAAiC,EAAGI,0BAA0B,GAAGL,SAASC;IAC5F,GACA;QAAEE,eAAe;IAAK;AAE1B;AAEA,wEAAwE;AACxE,0EAA0E;AAC1E,6EAA6E;AAC7E,+DAA+D;AAC/D,SACEG,oCAAoC,EACpCC,yBAAyB,QAEpB,mCAA+B;AACtC,SACEC,mBAAmB,EACnBC,mBAAmB,QAEd,8BAA0B;AAOjC,SACEC,mBAAmB,EACnBC,qBAAqB,EACrBC,0BAA0B,EAC1BC,mBAAmB,QAGd,yBAAqB;AAC5B,SACEC,yBAAyB,EACzBC,mBAAmB,QAGd,8BAA0B;AAEjC,iEAAiE;AACjE,6EAA6E;AAC7E,uEAAuE;AACvE,4DAA4D;AAC5D,SACEC,6BAA6B,EAC7BC,iCAAiC,EACjCC,+BAA+B,QAG1B,yCAAqC;AAC5C,SACEC,uBAAuB,EACvBC,6BAA6B,EAC7BC,4BAA4B,EAC5BC,sBAAsB,EACtBC,+BAA+B,EAC/BC,0BAA0B,EAC1BC,iCAAiC,EACjCC,8BAA8B,EAC9BC,oCAAoC,EACpCC,mCAAmC,EACnCC,6BAA6B,EAC7BC,gCAAgC,QAK3B,oCAAgC"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/outreach/src/lib/server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Outreach's SERVER half (AGL-2974), `@aglyn/plugins-outreach/server`.\n *\n * Every write to an `outreach*` collection is made here: the Firestore rules\n * refuse all of them to clients, so a mailbox, a sequence, an enrollment and a\n * credential only ever change through a handler registered below, behind the\n * session, entitlement and permission checks that handler makes.\n *\n * `outreach/ping` exists so the wiring is proven rather than assumed, as the\n * CRM's does: `plugins.config.json` names `registerOutreachConsoleApi` and the\n * `outreach` prefix, the generated server manifest loads this module, and the\n * console's `/api/[...pluginApi]` dispatcher reaches the handler — after its\n * own gates, which already refuse a request while `release_outreach` is off\n * for the caller. A first real route that also had to be the first wiring\n * test would be two things to debug at once.\n *\n * A route that reads or writes org data resolves the caller the way the CRM's\n * org routes do (`resolveOrgMembership` / `memberHasOrgPermission` from\n * `@aglyn/tenant-data-admin`) and asks `checkEntitlement(org, 'outreach')`\n * and `outreach.use` before it touches a document.\n */\n\nimport {\n registerPluginApiRoute,\n registerPluginPermissions,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { OUTREACH_API_ROUTES } from './constants/api-routes'\nimport {\n OUTREACH_PERMISSIONS,\n OUTREACH_PLUGIN_ID,\n} from './constants/bundle-common'\nimport { registerOutreachMailboxRoutes } from './mailboxes/register-mailbox-routes'\nimport { registerOutreachRoutes } from './routes/register-routes'\n\n/**\n * No auth, no org, no data: it answers whether this server bundle was loaded\n * and its routes registered, which is a fact about the process rather than\n * about any workspace.\n */\nexport const outreachPingHandler: PluginApiHandler = (req, res) => {\n if (req.method !== 'GET') {\n res.setHeader('Allow', 'GET')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n res.status(200).json({ ok: true, plugin: OUTREACH_PLUGIN_ID })\n}\n\n/** Console API registration, named in `plugins.config.json` as `consoleApi`. */\nexport function registerOutreachConsoleApi(): void {\n // The server resolves plugin permission keys from this registry, so a\n // route's `outreach.use` check and the console's gate read the same tier\n // defaults.\n registerPluginPermissions(OUTREACH_PERMISSIONS)\n registerPluginApiRoute(OUTREACH_API_ROUTES.ping, outreachPingHandler)\n // Connect a Google mailbox, its settings, pause, test and disconnect\n // (AGL-2978).\n registerOutreachMailboxRoutes()\n // Compliance settings, sequences, enrollment and enrollment actions\n // (AGL-2980).\n registerOutreachRoutes()\n // The one-click unsubscribe every email carries (AGL-2981): a recipient\n // link, so it answers whether or not Outreach is released to the\n // organization now. Its module loads on the first request.\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.unsubscribe,\n {\n web: async (request, context) =>\n (await import('./runtime/platform-runtime-deps')).platformOutreachUnsubscribeRoute()(request, context),\n },\n { recipientLink: true },\n )\n // A link in a tracked sequence email (AGL-3239): a recipient link for the\n // same reason, and more pressingly — a link already sitting in somebody's\n // inbox has to keep forwarding them whether or not the rollout is paused.\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.click,\n {\n web: async (request, context) =>\n (await import('./runtime/platform-runtime-deps')).platformOutreachClickRoute()(request, context),\n },\n { recipientLink: true },\n )\n // The short tracking link every send carries since AGL-3297, for the same\n // reasons; the signed `click` route above keeps answering older emails.\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.shortLink,\n {\n web: async (request, context) =>\n (await import('./runtime/platform-runtime-deps')).platformOutreachShortLinkRoute()(request, context),\n },\n { recipientLink: true },\n )\n}\n\n// The transport the sending runtime reaches a connected mailbox through\n// (AGL-2978): open a mailbox's Gmail client, mark one reconnect-required,\n// send the engine's composed email through the one RFC 5322 writer, and read\n// a thread whole for the engine's reply and bounce classifier.\nexport {\n markOutreachMailboxReconnectRequired,\n openOutreachMailboxClient,\n type OpenedOutreachMailbox,\n} from './mailboxes/mailbox-transport'\nexport {\n GmailTransportError,\n isReconnectRequired,\n type GmailTransportErrorCode,\n} from './transport/gmail-errors'\nexport type {\n GmailClient,\n GmailFullThread,\n GmailMessageMetadata,\n GmailThread,\n} from './transport/gmail-client'\nexport {\n buildRfc5322Message,\n encodeGmailRawMessage,\n LIST_UNSUBSCRIBE_ONE_CLICK,\n Rfc5322MessageError,\n type OutreachComposedMessage,\n type OutreachMailAddress,\n} from './transport/rfc5322'\nexport {\n sendComposedOutreachEmail,\n sendOutreachMessage,\n type OutreachSentMessage,\n type SendOutreachMessageOptions,\n} from './transport/send-message'\n\n// The organization's compliance settings and do-not-contact list\n// (AGL-2980), for the sending runtime: the footer is read before every send,\n// the list is checked before every send, and a reply that opts out, an\n// unsubscribe and a hard bounce each put the address on it.\nexport {\n outreachComplianceSettingsRef,\n readOutreachComplianceSettingsDoc,\n writeOutreachComplianceSettings,\n type WriteOutreachComplianceSettingsInput,\n type WriteOutreachComplianceSettingsResult,\n} from './storage/compliance-settings-store'\nexport {\n addOutreachDoNotContact,\n addOutreachDoNotContactDomain,\n getOutreachDoNotContactEntry,\n isOutreachDoNotContact,\n listOutreachDoNotContactDomains,\n lookupOutreachDoNotContact,\n lookupOutreachDoNotContactDomains,\n outreachDoNotContactCollection,\n outreachDoNotContactDomainCollection,\n readOutreachDoNotContactDomainEntry,\n readOutreachDoNotContactEntry,\n removeOutreachDoNotContactDomain,\n type AddOutreachDoNotContactDomainInput,\n type AddOutreachDoNotContactDomainResult,\n type AddOutreachDoNotContactInput,\n type AddOutreachDoNotContactResult,\n} from './storage/do-not-contact-store'\n"],"names":["registerPluginApiRoute","registerPluginPermissions","OUTREACH_API_ROUTES","OUTREACH_PERMISSIONS","OUTREACH_PLUGIN_ID","registerOutreachMailboxRoutes","registerOutreachRoutes","outreachPingHandler","req","res","method","setHeader","status","json","error","ok","plugin","registerOutreachConsoleApi","ping","unsubscribe","web","request","context","platformOutreachUnsubscribeRoute","recipientLink","click","platformOutreachClickRoute","shortLink","platformOutreachShortLinkRoute","markOutreachMailboxReconnectRequired","openOutreachMailboxClient","GmailTransportError","isReconnectRequired","buildRfc5322Message","encodeGmailRawMessage","LIST_UNSUBSCRIBE_ONE_CLICK","Rfc5322MessageError","sendComposedOutreachEmail","sendOutreachMessage","outreachComplianceSettingsRef","readOutreachComplianceSettingsDoc","writeOutreachComplianceSettings","addOutreachDoNotContact","addOutreachDoNotContactDomain","getOutreachDoNotContactEntry","isOutreachDoNotContact","listOutreachDoNotContactDomains","lookupOutreachDoNotContact","lookupOutreachDoNotContactDomains","outreachDoNotContactCollection","outreachDoNotContactDomainCollection","readOutreachDoNotContactDomainEntry","readOutreachDoNotContactEntry","removeOutreachDoNotContactDomain"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;CAoBC,GAED,SACEA,sBAAsB,EACtBC,yBAAyB,QAEpB,sBAAqB;AAC5B,SAASC,mBAAmB,QAAQ,4BAAwB;AAC5D,SACEC,oBAAoB,EACpBC,kBAAkB,QACb,+BAA2B;AAClC,SAASC,6BAA6B,QAAQ,yCAAqC;AACnF,SAASC,sBAAsB,QAAQ,8BAA0B;AAEjE;;;;CAIC,GACD,OAAO,MAAMC,sBAAwC,CAACC,KAAKC;IACzD,IAAID,IAAIE,MAAM,KAAK,OAAO;QACxBD,IAAIE,SAAS,CAAC,SAAS;QACvBF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;QACnD;IACF;IACAL,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEE,IAAI;QAAMC,QAAQZ;IAAmB;AAC9D,EAAC;AAED,8EAA8E,GAC9E,OAAO,SAASa;IACd,sEAAsE;IACtE,yEAAyE;IACzE,YAAY;IACZhB,0BAA0BE;IAC1BH,uBAAuBE,oBAAoBgB,IAAI,EAAEX;IACjD,qEAAqE;IACrE,cAAc;IACdF;IACA,oEAAoE;IACpE,cAAc;IACdC;IACA,wEAAwE;IACxE,iEAAiE;IACjE,2DAA2D;IAC3DN,uBACEE,oBAAoBiB,WAAW,EAC/B;QACEC,KAAK,OAAOC,SAASC,UACnB,AAAC,CAAA,MAAM,MAAM,CAAC,qCAAiC,EAAGC,gCAAgC,GAAGF,SAASC;IAClG,GACA;QAAEE,eAAe;IAAK;IAExB,0EAA0E;IAC1E,0EAA0E;IAC1E,0EAA0E;IAC1ExB,uBACEE,oBAAoBuB,KAAK,EACzB;QACEL,KAAK,OAAOC,SAASC,UACnB,AAAC,CAAA,MAAM,MAAM,CAAC,qCAAiC,EAAGI,0BAA0B,GAAGL,SAASC;IAC5F,GACA;QAAEE,eAAe;IAAK;IAExB,0EAA0E;IAC1E,wEAAwE;IACxExB,uBACEE,oBAAoByB,SAAS,EAC7B;QACEP,KAAK,OAAOC,SAASC,UACnB,AAAC,CAAA,MAAM,MAAM,CAAC,qCAAiC,EAAGM,8BAA8B,GAAGP,SAASC;IAChG,GACA;QAAEE,eAAe;IAAK;AAE1B;AAEA,wEAAwE;AACxE,0EAA0E;AAC1E,6EAA6E;AAC7E,+DAA+D;AAC/D,SACEK,oCAAoC,EACpCC,yBAAyB,QAEpB,mCAA+B;AACtC,SACEC,mBAAmB,EACnBC,mBAAmB,QAEd,8BAA0B;AAOjC,SACEC,mBAAmB,EACnBC,qBAAqB,EACrBC,0BAA0B,EAC1BC,mBAAmB,QAGd,yBAAqB;AAC5B,SACEC,yBAAyB,EACzBC,mBAAmB,QAGd,8BAA0B;AAEjC,iEAAiE;AACjE,6EAA6E;AAC7E,uEAAuE;AACvE,4DAA4D;AAC5D,SACEC,6BAA6B,EAC7BC,iCAAiC,EACjCC,+BAA+B,QAG1B,yCAAqC;AAC5C,SACEC,uBAAuB,EACvBC,6BAA6B,EAC7BC,4BAA4B,EAC5BC,sBAAsB,EACtBC,+BAA+B,EAC/BC,0BAA0B,EAC1BC,iCAAiC,EACjCC,8BAA8B,EAC9BC,oCAAoC,EACpCC,mCAAmC,EACnCC,6BAA6B,EAC7BC,gCAAgC,QAK3B,oCAAgC"}