@aglyn/plugins-outreach 1.0.0-beta.164 → 1.0.0-beta.166
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/lib/engine/enrollment-activity.d.ts +61 -0
- package/src/lib/engine/enrollment-activity.js +57 -0
- package/src/lib/engine/enrollment-activity.js.map +1 -0
- package/src/lib/enrollment/enroll-people.js +2 -1
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/routes/enroll-routes.d.ts +7 -0
- package/src/lib/routes/enroll-routes.js +53 -7
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/preview-routes.js +3 -1
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +10 -1
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/runtime/enrollment-events.d.ts +5 -3
- package/src/lib/runtime/enrollment-events.js +27 -3
- package/src/lib/runtime/enrollment-events.js.map +1 -1
- package/src/lib/runtime/lead-records.d.ts +1 -1
- package/src/lib/runtime/lead-records.js +3 -2
- package/src/lib/runtime/lead-records.js.map +1 -1
- package/src/lib/runtime/sync-job.js +24 -2
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/runtime/timeline.d.ts +14 -0
- package/src/lib/runtime/timeline.js +28 -0
- package/src/lib/runtime/timeline.js.map +1 -1
- package/src/lib/runtime/unsubscribe-route.d.ts +1 -1
- package/src/lib/runtime/unsubscribe-route.js.map +1 -1
|
@@ -19,6 +19,7 @@ import { crmInboundExcerpt, emailAddressOf } from "@aglyn/aglyn/app-utils/crm-in
|
|
|
19
19
|
import { outreachMessageIdHeader } from "../engine/compose.js";
|
|
20
20
|
import { readOutreachDeliveryReport } from "../engine/delivery-status.js";
|
|
21
21
|
import { outreachEmailDomain } from "../engine/do-not-contact-domain.js";
|
|
22
|
+
import { outreachStoppedEntry } from "../engine/enrollment-activity.js";
|
|
22
23
|
import { isOutreachGatewayBlock, outreachGatewayBlockDetail } from "../engine/gateway-block.js";
|
|
23
24
|
import { effectiveOutreachWindow, postponeOutreachForAutoReply } from "../engine/schedule.js";
|
|
24
25
|
import { decideOutreachThread } from "../engine/thread-classification.js";
|
|
@@ -33,7 +34,7 @@ import { applyOutreachEvent, recordOutreachOptOut } from "./enrollment-events.js
|
|
|
33
34
|
import { applyOutreachMailboxHealth, emptyOutreachHealthDelta } from "./mailbox-health-store.js";
|
|
34
35
|
import { noteOutreachMailboxReconnectRequired } from "./mailbox-notices.js";
|
|
35
36
|
import { markOutreachLeadWorking } from "./lead-records.js";
|
|
36
|
-
import { fileOutreachEmail, fileOutreachTask, markOutreachEmailDelivery } from "./timeline.js";
|
|
37
|
+
import { fileOutreachEmail, fileOutreachNote, fileOutreachTask, markOutreachEmailDelivery } from "./timeline.js";
|
|
37
38
|
import { outreachUnsubscribeMailbox } from "./unsubscribe-link.js";
|
|
38
39
|
/*==========================================
|
|
39
40
|
* THE SYNC JOB (AGL-2981): every fifteen minutes, on the console.
|
|
@@ -431,7 +432,7 @@ async function syncMailbox(deps, firestore, input) {
|
|
|
431
432
|
// A lead somebody wrote back to is being worked (AGL-3234).
|
|
432
433
|
if (enrollment.target === 'lead' && enrollment.leadId) {
|
|
433
434
|
await markOutreachLeadWorking(firestore, {
|
|
434
|
-
|
|
435
|
+
orgId: context.orgId,
|
|
435
436
|
leadId: enrollment.leadId
|
|
436
437
|
});
|
|
437
438
|
}
|
|
@@ -556,6 +557,27 @@ async function syncMailbox(deps, firestore, input) {
|
|
|
556
557
|
nowMs
|
|
557
558
|
});
|
|
558
559
|
if (postponedTo !== null && ((_outcome_enrollment = outcome.enrollment) == null ? void 0 : _outcome_enrollment.nextDueAtMs) === postponedTo) report.postponed += 1;
|
|
560
|
+
// The person's record says the sequence ended (AGL-3274), once, under the
|
|
561
|
+
// reply that ended it — after the transaction moved the status, so a run
|
|
562
|
+
// that reads the same reply again finds nothing to file. An opt-out's
|
|
563
|
+
// line is `recordOutreachOptOut`'s, beside the lists it writes.
|
|
564
|
+
if ((event == null ? void 0 : event.type) === 'reply' && outcome.changed) {
|
|
565
|
+
var _ref7;
|
|
566
|
+
const sequence = await sequenceOf(context, enrollment.sequenceId);
|
|
567
|
+
const entry = outreachStoppedEntry({
|
|
568
|
+
enrollmentId: enrollment.id,
|
|
569
|
+
sequenceName: (_ref7 = sequence == null ? void 0 : sequence.name) != null ? _ref7 : '',
|
|
570
|
+
reason: 'replied'
|
|
571
|
+
});
|
|
572
|
+
await fileOutreachNote(deps, {
|
|
573
|
+
orgId: context.orgId,
|
|
574
|
+
hostId: enrollment.hostId,
|
|
575
|
+
link: outreachEnrollmentLink(enrollment),
|
|
576
|
+
dedupeKey: entry.dedupeKey,
|
|
577
|
+
body: entry.body,
|
|
578
|
+
atMs: event.atMs
|
|
579
|
+
});
|
|
580
|
+
}
|
|
559
581
|
// The run's own view of the enrollment moves with it, so a second message
|
|
560
582
|
// of it in this run is judged against what this one did.
|
|
561
583
|
if (outcome.enrollment) Object.assign(enrollment, outcome.enrollment);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/sync-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 { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { isPublicMailboxDomain } from '@aglyn/aglyn/app-utils/crm'\nimport { crmInboundExcerpt, emailAddressOf } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport { outreachMessageIdHeader } from '../engine/compose'\nimport { readOutreachDeliveryReport } from '../engine/delivery-status'\nimport { outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport type { OutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport { isOutreachGatewayBlock, outreachGatewayBlockDetail } from '../engine/gateway-block'\nimport { effectiveOutreachWindow, postponeOutreachForAutoReply } from '../engine/schedule'\nimport {\n decideOutreachThread,\n type OutreachMessageClassification,\n type OutreachThreadDecision,\n} from '../engine/thread-classification'\nimport { outreachHeader, outreachThreadMessageFromGmail, type OutreachThreadMessage } from '../engine/thread-message'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport { addOutreachDoNotContact, addOutreachDoNotContactDomain } from '../storage/do-not-contact-store'\nimport {\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport type { GmailClient } from '../transport/gmail-client'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { creditOutreachReply } from './campaign-credit'\nimport { applyOutreachEvent, recordOutreachOptOut } from './enrollment-events'\nimport {\n applyOutreachMailboxHealth,\n emptyOutreachHealthDelta,\n type OutreachMailboxHealthDelta,\n} from './mailbox-health-store'\nimport { noteOutreachMailboxReconnectRequired } from './mailbox-notices'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { markOutreachLeadWorking } from './lead-records'\nimport { fileOutreachEmail, fileOutreachTask, markOutreachEmailDelivery } from './timeline'\nimport { outreachUnsubscribeMailbox } from './unsubscribe-link'\n\n/*==========================================\n * THE SYNC JOB (AGL-2981): every fifteen minutes, on the console.\n *\n * For each connected or paused mailbox, what came back since the last run —\n * read with `format=full`, turned into the engine's message shape and\n * classified by `decideOutreachThread`:\n *\n * - a REPLY stops the enrollment, files the message on the contact's\n * timeline (once per `Message-ID`, under the capture address's own id for\n * it) and gives the rep the task \"Reply from <name>\";\n * - an OUT-OF-OFFICE postpones the next step a working week from the reply;\n * - an OPT-OUT — a reply asking to be left alone, or a message to the\n * `+unsubscribe` address the `mailto:` names — puts the address on the\n * organization's do-not-contact list and off the site's `sales` topic,\n * and stops every open enrollment of it;\n * - a HARD BOUNCE stops the enrollment, files the address on the platform's\n * suppression list and the do-not-contact list — and, when its diagnostic\n * reads as the domain's mail gateway refusing the sender rather than one\n * address being unknown (`engine/gateway-block`, AGL-3244), files the\n * DOMAIN on the do-not-contact list too, and says so on the enrollment.\n *\n * Every verdict a list takes is said on the record the person is as well\n * (AGL-3245): the lead and the contact carry `emailState` through the\n * platform's stamp, and a bounce or a complaint lands on the send's own\n * timeline entry, so a member reading the record sees what the runtime saw.\n *\n * Four reads find those, each narrowed by Gmail rather than read in full:\n * the threads of this mailbox's enrollments that have new mail; delivery\n * reports outside those threads (a receiving server's own bounce), by\n * searching for the mail systems that send them; messages to the\n * unsubscribe address; and messages from the people this mailbox is\n * writing to, in a thread of their own.\n *\n * Every write it makes is idempotent — the do-not-contact list keeps its\n * first entry, a topic opt-out keeps its first moment, a timeline entry is\n * filed once per message — and the messages it handled are marked AFTER\n * their effects, on the enrollment and on the mailbox, so a run that dies\n * part-way repeats harmless work rather than skipping some. The mailbox's\n * health takes the run's bounces and complaints in one write at the end,\n * which is where it pauses itself.\n *==========================================*/\n\nexport { OUTREACH_SYNC_JOB_ID } from '../constants/runtime-jobs'\n\n/** The furthest back a run reads: a mailbox's first run, or one that fell behind. */\nexport const OUTREACH_SYNC_LOOKBACK_MS = 7 * 24 * 60 * 60_000\n\n/** How far a run reads back past the previous one, for mail Gmail indexed late. */\nexport const OUTREACH_SYNC_OVERLAP_MS = 2 * 60 * 60_000\n\n/** How long after its last send an enrollment's mail is still watched. */\nexport const OUTREACH_SYNC_WATCH_MS = 45 * 24 * 60 * 60_000\n\n/** The most pages of one search a run reads. */\nconst SEARCH_PAGES = 5\n/** Handled message ids a mailbox keeps, and an enrollment keeps. */\nconst MAILBOX_HANDLED_MAX = 500\nconst ENROLLMENT_HANDLED_MAX = 100\n/** Addresses in one `from:` search. */\nconst FROM_CHUNK = 15\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface OutreachSyncReport {\n [key: string]: number\n mailboxes: number\n threads: number\n replies: number\n optOuts: number\n bounces: number\n /** Hard bounces that read as a gateway block, each filing its domain (AGL-3244). */\n gatewayBlocks: number\n postponed: number\n paused: number\n}\n\ninterface SyncContext {\n deps: OutreachRuntimeDeps\n firestore: Firestore\n orgId: string\n mailbox: OutreachMailbox\n client: GmailClient\n selfAddresses: string[]\n unsubscribeAddress: string | null\n sequences: Map<string, OutreachSequence | null>\n delta: OutreachMailboxHealthDelta\n report: OutreachSyncReport\n nowMs: number\n}\n\n/** Runs the sync job once — see the module note. */\nexport async function runOutreachSyncJob(\n deps: OutreachRuntimeDeps,\n context: { nowMs: number; deadlineMs: number },\n): Promise<OutreachSyncReport> {\n const firestore = deps.firestore()\n const report: OutreachSyncReport = {\n mailboxes: 0,\n threads: 0,\n replies: 0,\n optOuts: 0,\n bounces: 0,\n gatewayBlocks: 0,\n postponed: 0,\n paused: 0,\n }\n const all = await firestore.collectionGroup(OUTREACH_COLLECTIONS.mailboxes).get()\n const byOrg = new Map<string, OutreachMailbox[]>()\n for (const doc of all.docs) {\n const orgId = doc.ref.parent.parent?.id\n const mailbox = readStoredOutreachMailbox(doc.id, doc.data())\n if (!orgId || !mailbox || (mailbox.status !== 'connected' && mailbox.status !== 'paused')) continue\n byOrg.set(orgId, [...(byOrg.get(orgId) ?? []), mailbox])\n }\n for (const [orgId, mailboxes] of byOrg) {\n if (deps.now() >= context.deadlineMs) break\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))) continue\n for (const mailbox of mailboxes) {\n if (deps.now() >= context.deadlineMs) break\n try {\n await syncMailbox(deps, firestore, { orgId, mailbox, report, nowMs: context.nowMs })\n report.mailboxes += 1\n } catch (error) {\n // One mailbox's failure is its own; the next mailbox is read. A grant\n // Google refused is the mailbox's to reconnect.\n if (isReconnectRequired(error)) {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: error instanceof GmailTransportError ? error.code : 'invalid_grant',\n nowMs: context.nowMs,\n })\n }\n console.error('[outreach] syncing a mailbox failed', error)\n }\n }\n }\n return report\n}\n\n/** Every stub a search finds, a few pages deep. */\nasync function search(\n client: GmailClient,\n q: string,\n): Promise<Array<{ id: string; threadId: string }>> {\n const found: Array<{ id: string; threadId: string }> = []\n let pageToken: string | null = null\n for (let page = 0; page < SEARCH_PAGES; page += 1) {\n const list = await client.listMessages({ q, maxResults: 100, pageToken, includeSpamTrash: true })\n found.push(...list.messages.filter((message) => message.id))\n pageToken = list.nextPageToken\n if (!pageToken) break\n }\n return found\n}\n\nasync function syncMailbox(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n input: { orgId: string; mailbox: OutreachMailbox; report: OutreachSyncReport; nowMs: number },\n): Promise<void> {\n const { orgId, mailbox, report, nowMs } = input\n const opened = await deps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n if (opened.reason !== 'not-configured') {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n }\n return\n }\n const unsubscribe = outreachUnsubscribeMailbox(mailbox.email)\n const context: SyncContext = {\n deps,\n firestore,\n orgId,\n mailbox,\n client: opened.client,\n selfAddresses: [\n mailbox.email,\n mailbox.sendAs,\n ...(mailbox.sendAsOptions ?? []).map((option) => option.email),\n ].filter(Boolean),\n unsubscribeAddress: unsubscribe?.address ?? null,\n sequences: new Map(),\n delta: emptyOutreachHealthDelta(),\n report,\n nowMs,\n }\n\n // The enrollments this mailbox is writing to, by thread and by address.\n const watched = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('mailboxId', '==', mailbox.id)\n .where('lastSentAtMs', '>=', nowMs - OUTREACH_SYNC_WATCH_MS)\n .get()\n const byThread = new Map<string, OutreachEnrollment>()\n const byAddress = new Map<string, OutreachEnrollment>()\n for (const doc of watched.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment) continue\n for (const threadId of enrollment.gmailThreadIds) byThread.set(threadId, enrollment)\n const known = byAddress.get(enrollment.email)\n if (!known || (enrollment.lastSentAtMs ?? 0) > (known.lastSentAtMs ?? 0)) byAddress.set(enrollment.email, enrollment)\n }\n\n const sinceMs = Math.max(Number(mailbox.sync?.throughMs) || 0, nowMs - OUTREACH_SYNC_LOOKBACK_MS) - OUTREACH_SYNC_OVERLAP_MS\n const after = `after:${Math.floor(sinceMs / 1000)}`\n // Every message this run or an earlier one handled outside an enrollment's\n // thread; one message is handled once however many searches find it.\n const handled = new Set(mailbox.sync?.handledMessageIds ?? [])\n const newlyHandled: string[] = []\n const markHandled = (id: string) => {\n if (handled.has(id)) return\n handled.add(id)\n newlyHandled.push(id)\n }\n const fresh = (stub: { id: string; threadId: string }) => !handled.has(stub.id) && !byThread.has(stub.threadId)\n\n // 1. The enrollments' own threads that have new mail.\n const inbound = await search(context.client, `${after} -from:me`)\n for (const threadId of new Set(inbound.map((stub) => stub.threadId))) {\n const enrollment = byThread.get(threadId)\n if (!enrollment) continue\n const thread = await context.client.getFullThread(threadId)\n report.threads += 1\n await applyMessages(context, enrollment, thread.messages.map(outreachThreadMessageFromGmail))\n }\n\n // 2. Delivery reports outside those threads: a receiving server's bounce.\n for (const stub of await search(context.client, `${after} from:(mailer-daemon OR postmaster)`)) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const delivery = readOutreachDeliveryReport(message)\n if (delivery?.kind === 'hard') {\n for (const address of delivery.failedAddresses) {\n const enrollment = byAddress.get(address)\n if (enrollment) await applyMessages(context, enrollment, [message])\n }\n }\n markHandled(stub.id)\n }\n\n // 3. Messages to the unsubscribe address: whoever wrote asked to leave.\n if (unsubscribe) {\n for (const stub of await search(context.client, `${after} to:${unsubscribe.address}`)) {\n if (handled.has(stub.id)) continue\n const metadata = await context.client.getMessage(stub.id, { metadataHeaders: ['From'] })\n const from = emailAddressOf(metadata.headers.find((header) => header.name.toLowerCase() === 'from')?.value ?? '')\n const address = normalizeContactEmail(from)\n if (address && !context.selfAddresses.includes(address)) {\n const enrollment = byAddress.get(address) ?? null\n await recordOutreachOptOut(deps, {\n orgId,\n email: address,\n source: 'unsubscribe',\n enrollment,\n hostIds: enrollment ? [enrollment.hostId] : [],\n detail: 'Wrote to the unsubscribe address.',\n nowMs,\n })\n report.optOuts += 1\n }\n markHandled(stub.id)\n }\n }\n\n // 4. The people this mailbox writes to, answering in a thread of their own.\n const addresses = [...byAddress.keys()]\n for (let start = 0; start < addresses.length; start += FROM_CHUNK) {\n const chunk = addresses.slice(start, start + FROM_CHUNK)\n for (const stub of await search(context.client, `${after} from:(${chunk.join(' OR ')})`)) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const enrollment = byAddress.get(normalizeContactEmail(emailAddressOf(message.from)) ?? '')\n if (enrollment) await applyMessages(context, enrollment, [message])\n markHandled(stub.id)\n }\n }\n\n const paused = await applyOutreachMailboxHealth(deps, { orgId, mailboxId: mailbox.id, delta: context.delta })\n if (paused) report.paused += 1\n await mailboxRef(firestore, orgId, mailbox.id).update({\n sync: {\n throughMs: nowMs,\n handledMessageIds: [...(mailbox.sync?.handledMessageIds ?? []), ...newlyHandled].slice(-MAILBOX_HANDLED_MAX),\n },\n })\n}\n\n/** The sequence an enrollment runs through, read once per run. */\nasync function sequenceOf(context: SyncContext, sequenceId: string): Promise<OutreachSequence | null> {\n if (!context.sequences.has(sequenceId)) {\n const snapshot = await outreachOrgCollection(context.firestore, context.orgId, 'sequences').doc(sequenceId).get()\n context.sequences.set(sequenceId, readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined))\n }\n return context.sequences.get(sequenceId) ?? null\n}\n\n/** A message's `Message-ID` in header form, or `null` when it carries none. */\nconst messageIdOf = (message: OutreachThreadMessage) => outreachMessageIdHeader(outreachHeader(message, 'Message-ID'))\n\n/** A bounce's diagnostic with the recipient's address taken out of it. */\nexport function scrubbedOutreachDiagnostic(diagnostic: string | null, email: string): string | null {\n if (!diagnostic) return null\n const pattern = new RegExp(email.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'gi')\n return diagnostic.replace(pattern, 'the address')\n}\n\n/** Files a message the person wrote on their timeline. */\nasync function fileInbound(context: SyncContext, enrollment: OutreachEnrollment, message: OutreachThreadMessage) {\n const messageId = messageIdOf(message)\n if (!messageId) return\n await fileOutreachEmail(context.deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n direction: 'inbound',\n subject: message.subject,\n from: emailAddressOf(message.from) ?? message.from,\n to: context.mailbox.sendAs || context.mailbox.email,\n messageId,\n inReplyTo: outreachMessageIdHeader(outreachHeader(message, 'In-Reply-To')),\n body: crmInboundExcerpt(message.textBody ?? message.snippet ?? '', message.htmlBody ?? ''),\n atMs: message.internalDateMs,\n byUid: '',\n })\n}\n\n/**\n * What the new messages of one enrollment mean, done: the effects first,\n * each idempotent, then the event and the handled ids in one transaction.\n */\nasync function applyMessages(\n context: SyncContext,\n enrollment: OutreachEnrollment,\n messages: readonly OutreachThreadMessage[],\n): Promise<void> {\n const { deps, firestore, report, nowMs } = context\n const decision: OutreachThreadDecision = decideOutreachThread({\n messages,\n recipient: enrollment.email,\n handledMessageIds: enrollment.syncedMessageIds ?? [],\n selfAddresses: context.selfAddresses,\n unsubscribeAddress: context.unsubscribeAddress,\n threadSubject: enrollment.threadSubject,\n })\n const freshIds = decision.classifications.map((classification) => classification.messageId).filter(Boolean)\n if (!freshIds.length) return\n const decidedBy: OutreachMessageClassification | null = decision.decidedBy\n const decidingMessage = decidedBy ? messages.find((message) => message.id === decidedBy.messageId) ?? null : null\n const lastSentMessageId = enrollment.messageIds[enrollment.messageIds.length - 1] ?? null\n\n for (const classification of decision.classifications) {\n const aboutRecipient =\n !classification.bounce?.recipients.length || classification.bounce.recipients.includes(enrollment.email)\n if (classification.kind === 'hard_bounce' && aboutRecipient) {\n context.delta.bounces.push({ atMs: classification.atMs || nowMs, messageId: lastSentMessageId })\n }\n if (classification.complaint) {\n context.delta.complaintAtMs = Math.max(context.delta.complaintAtMs ?? 0, classification.atMs || nowMs)\n }\n }\n\n let event: OutreachEnrollmentEvent | null = null\n let postponedTo: number | null = null\n switch (decision.outcome) {\n case 'opted_out': {\n if (decidingMessage) await fileInbound(context, enrollment, decidingMessage)\n const complaint = decidedBy?.complaint === true\n await recordOutreachOptOut(deps, {\n orgId: context.orgId,\n email: enrollment.email,\n source: decidedBy?.evidence?.startsWith('To:') ? 'unsubscribe' : 'opt_out_reply',\n enrollment,\n hostIds: [enrollment.hostId],\n detail: decidedBy?.evidence ?? null,\n nowMs,\n complaint,\n })\n // A reply that called the email spam marks the send it answered\n // (AGL-3245), as the campaign webhook would mark a reported one.\n if (complaint) {\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'complained',\n atMs: decidedBy?.atMs || nowMs,\n detail: decidedBy?.evidence ?? null,\n })\n }\n report.optOuts += 1\n break\n }\n case 'replied': {\n if (decidingMessage) {\n await fileInbound(context, enrollment, decidingMessage)\n try {\n await fileOutreachTask(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: `reply:${messageIdOf(decidingMessage) ?? decidingMessage.id}`,\n title: `Reply from ${enrollment.contactName || enrollment.email}`,\n notes: crmInboundExcerpt(decidingMessage.textBody ?? decidingMessage.snippet ?? '', decidingMessage.htmlBody ?? '').slice(0, 500),\n kind: 'email',\n dueAtMs: nowMs,\n assigneeUid: context.mailbox.connectedByUid,\n })\n } catch (error) {\n console.error('[outreach] the reply task could not be filed', error)\n }\n }\n // A lead somebody wrote back to is being worked (AGL-3234).\n if (enrollment.target === 'lead' && enrollment.leadId) {\n await markOutreachLeadWorking(firestore, { hostId: enrollment.hostId, leadId: enrollment.leadId })\n }\n context.delta.replies += 1\n event = { type: 'reply', atMs: decidedBy?.atMs || nowMs, detail: decidedBy?.evidence ?? null }\n report.replies += 1\n // Once per enrollment (AGL-3254): a reply moves the status to\n // `replied`, and nothing is synced for it again.\n await creditOutreachReply(deps, { enrollment, atMs: event.atMs })\n break\n }\n case 'bounced': {\n const diagnostic = scrubbedOutreachDiagnostic(decidedBy?.bounce?.diagnostic ?? decidedBy?.bounce?.status ?? null, enrollment.email)\n await deps.suppressBouncedEmail({ email: enrollment.email, hostId: enrollment.hostId })\n await addOutreachDoNotContact(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n reason: 'hard_bounce',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n let detail = diagnostic\n // A gateway block is the domain's verdict on the sender (AGL-3244):\n // the domain is filed beside the address, and the enrollment says so.\n // Never a public mailbox provider's domain — Gmail refusing one\n // message on policy is not a reason to stop writing to Gmail.\n const domain = outreachEmailDomain(enrollment.email)\n const gateway = isOutreachGatewayBlock(decidedBy?.bounce ?? null)\n if (domain && !isPublicMailboxDomain(domain) && gateway) {\n await addOutreachDoNotContactDomain(firestore, {\n orgId: context.orgId,\n domain,\n reason: 'gateway_block',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n detail = outreachGatewayBlockDetail(domain, diagnostic)\n report.gatewayBlocks += 1\n }\n const bouncedAtMs = decidedBy?.atMs || nowMs\n // The record the person is says so too (AGL-3245): the verdict on the\n // lead and the contact, and the bounce on the send's own timeline\n // entry, so the record reads Sent, then Bounced.\n await deps.stampRecordEmailState({\n orgId: context.orgId,\n email: enrollment.email,\n state: {\n status: gateway ? 'blocked' : 'bounced',\n atMs: bouncedAtMs,\n source: 'sequence',\n detail: diagnostic,\n enrollmentId: enrollment.id,\n },\n })\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'bounced',\n atMs: bouncedAtMs,\n detail: diagnostic,\n })\n event = { type: 'bounce', atMs: bouncedAtMs, detail }\n report.bounces += 1\n break\n }\n case 'postpone': {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n postponedTo = postponeOutreachForAutoReply({\n nextDueAtMs: enrollment.nextDueAtMs,\n receivedAtMs: decidedBy?.atMs || nowMs,\n timeZone: context.mailbox.timezone,\n window: effectiveOutreachWindow(sequence?.settings.window, context.mailbox.window),\n random: deps.random,\n })\n break\n }\n default:\n break\n }\n\n const outcome = await applyOutreachEvent(firestore, {\n orgId: context.orgId,\n enrollmentId: enrollment.id,\n event,\n extra: (current) => {\n const extra: Record<string, unknown> = {\n syncedMessageIds: [...(current.syncedMessageIds ?? []), ...freshIds].slice(-ENROLLMENT_HANDLED_MAX),\n }\n const waiting = current.status === 'active' || current.status === 'paused'\n if (postponedTo !== null && waiting && current.nextDueAtMs !== null && postponedTo > current.nextDueAtMs) {\n extra['nextDueAtMs'] = postponedTo\n }\n return extra\n },\n nowMs,\n })\n if (postponedTo !== null && outcome.enrollment?.nextDueAtMs === postponedTo) report.postponed += 1\n // The run's own view of the enrollment moves with it, so a second message\n // of it in this run is judged against what this one did.\n if (outcome.enrollment) Object.assign(enrollment, outcome.enrollment)\n}\n"],"names":["normalizeContactEmail","isPublicMailboxDomain","crmInboundExcerpt","emailAddressOf","outreachMessageIdHeader","readOutreachDeliveryReport","outreachEmailDomain","isOutreachGatewayBlock","outreachGatewayBlockDetail","effectiveOutreachWindow","postponeOutreachForAutoReply","decideOutreachThread","outreachHeader","outreachThreadMessageFromGmail","mailboxRef","OUTREACH_COLLECTIONS","addOutreachDoNotContact","addOutreachDoNotContactDomain","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","creditOutreachReply","applyOutreachEvent","recordOutreachOptOut","applyOutreachMailboxHealth","emptyOutreachHealthDelta","noteOutreachMailboxReconnectRequired","markOutreachLeadWorking","fileOutreachEmail","fileOutreachTask","markOutreachEmailDelivery","outreachUnsubscribeMailbox","OUTREACH_SYNC_JOB_ID","OUTREACH_SYNC_LOOKBACK_MS","OUTREACH_SYNC_OVERLAP_MS","OUTREACH_SYNC_WATCH_MS","SEARCH_PAGES","MAILBOX_HANDLED_MAX","ENROLLMENT_HANDLED_MAX","FROM_CHUNK","runOutreachSyncJob","deps","context","firestore","report","mailboxes","threads","replies","optOuts","bounces","gatewayBlocks","postponed","paused","all","collectionGroup","get","byOrg","Map","doc","docs","orgId","ref","parent","id","mailbox","data","status","set","now","deadlineMs","orgSnapshot","collection","org","exists","orgRefusal","syncMailbox","nowMs","error","mailboxId","errorCode","code","console","search","client","q","found","pageToken","page","list","listMessages","maxResults","includeSpamTrash","push","messages","filter","message","nextPageToken","input","opened","openMailbox","ok","reason","unsubscribe","email","selfAddresses","sendAs","sendAsOptions","map","option","Boolean","unsubscribeAddress","address","sequences","delta","watched","where","byThread","byAddress","enrollment","known","threadId","gmailThreadIds","lastSentAtMs","sinceMs","Math","max","Number","sync","throughMs","after","floor","handled","Set","handledMessageIds","newlyHandled","markHandled","has","add","fresh","stub","inbound","thread","getFullThread","applyMessages","getFullMessage","delivery","kind","failedAddresses","metadata","getMessage","metadataHeaders","from","headers","find","header","name","toLowerCase","value","includes","source","hostIds","hostId","detail","addresses","keys","start","length","chunk","slice","join","update","sequenceOf","sequenceId","snapshot","undefined","messageIdOf","scrubbedOutreachDiagnostic","diagnostic","pattern","RegExp","replace","fileInbound","messageId","link","direction","subject","to","inReplyTo","body","textBody","snippet","htmlBody","atMs","internalDateMs","byUid","outcome","decision","recipient","syncedMessageIds","threadSubject","freshIds","classifications","classification","decidedBy","decidingMessage","lastSentMessageId","messageIds","aboutRecipient","bounce","recipients","complaint","complaintAtMs","event","postponedTo","evidence","startsWith","state","dedupeKey","title","contactName","notes","dueAtMs","assigneeUid","connectedByUid","target","leadId","type","suppressBouncedEmail","enrollmentId","domain","gateway","bouncedAtMs","stampRecordEmailState","sequence","nextDueAtMs","receivedAtMs","timeZone","timezone","window","settings","random","extra","current","waiting","Object","assign"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/sync-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 { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { isPublicMailboxDomain } from '@aglyn/aglyn/app-utils/crm'\nimport { crmInboundExcerpt, emailAddressOf } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport { outreachMessageIdHeader } from '../engine/compose'\nimport { readOutreachDeliveryReport } from '../engine/delivery-status'\nimport { outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport { outreachStoppedEntry } from '../engine/enrollment-activity'\nimport type { OutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport { isOutreachGatewayBlock, outreachGatewayBlockDetail } from '../engine/gateway-block'\nimport { effectiveOutreachWindow, postponeOutreachForAutoReply } from '../engine/schedule'\nimport {\n decideOutreachThread,\n type OutreachMessageClassification,\n type OutreachThreadDecision,\n} from '../engine/thread-classification'\nimport { outreachHeader, outreachThreadMessageFromGmail, type OutreachThreadMessage } from '../engine/thread-message'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport { addOutreachDoNotContact, addOutreachDoNotContactDomain } from '../storage/do-not-contact-store'\nimport {\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport type { GmailClient } from '../transport/gmail-client'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { creditOutreachReply } from './campaign-credit'\nimport { applyOutreachEvent, recordOutreachOptOut } from './enrollment-events'\nimport {\n applyOutreachMailboxHealth,\n emptyOutreachHealthDelta,\n type OutreachMailboxHealthDelta,\n} from './mailbox-health-store'\nimport { noteOutreachMailboxReconnectRequired } from './mailbox-notices'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { markOutreachLeadWorking } from './lead-records'\nimport { fileOutreachEmail, fileOutreachNote, fileOutreachTask, markOutreachEmailDelivery } from './timeline'\nimport { outreachUnsubscribeMailbox } from './unsubscribe-link'\n\n/*==========================================\n * THE SYNC JOB (AGL-2981): every fifteen minutes, on the console.\n *\n * For each connected or paused mailbox, what came back since the last run —\n * read with `format=full`, turned into the engine's message shape and\n * classified by `decideOutreachThread`:\n *\n * - a REPLY stops the enrollment, files the message on the contact's\n * timeline (once per `Message-ID`, under the capture address's own id for\n * it) and gives the rep the task \"Reply from <name>\";\n * - an OUT-OF-OFFICE postpones the next step a working week from the reply;\n * - an OPT-OUT — a reply asking to be left alone, or a message to the\n * `+unsubscribe` address the `mailto:` names — puts the address on the\n * organization's do-not-contact list and off the site's `sales` topic,\n * and stops every open enrollment of it;\n * - a HARD BOUNCE stops the enrollment, files the address on the platform's\n * suppression list and the do-not-contact list — and, when its diagnostic\n * reads as the domain's mail gateway refusing the sender rather than one\n * address being unknown (`engine/gateway-block`, AGL-3244), files the\n * DOMAIN on the do-not-contact list too, and says so on the enrollment.\n *\n * Every verdict a list takes is said on the record the person is as well\n * (AGL-3245): the lead and the contact carry `emailState` through the\n * platform's stamp, and a bounce or a complaint lands on the send's own\n * timeline entry, so a member reading the record sees what the runtime saw.\n *\n * Four reads find those, each narrowed by Gmail rather than read in full:\n * the threads of this mailbox's enrollments that have new mail; delivery\n * reports outside those threads (a receiving server's own bounce), by\n * searching for the mail systems that send them; messages to the\n * unsubscribe address; and messages from the people this mailbox is\n * writing to, in a thread of their own.\n *\n * Every write it makes is idempotent — the do-not-contact list keeps its\n * first entry, a topic opt-out keeps its first moment, a timeline entry is\n * filed once per message — and the messages it handled are marked AFTER\n * their effects, on the enrollment and on the mailbox, so a run that dies\n * part-way repeats harmless work rather than skipping some. The mailbox's\n * health takes the run's bounces and complaints in one write at the end,\n * which is where it pauses itself.\n *==========================================*/\n\nexport { OUTREACH_SYNC_JOB_ID } from '../constants/runtime-jobs'\n\n/** The furthest back a run reads: a mailbox's first run, or one that fell behind. */\nexport const OUTREACH_SYNC_LOOKBACK_MS = 7 * 24 * 60 * 60_000\n\n/** How far a run reads back past the previous one, for mail Gmail indexed late. */\nexport const OUTREACH_SYNC_OVERLAP_MS = 2 * 60 * 60_000\n\n/** How long after its last send an enrollment's mail is still watched. */\nexport const OUTREACH_SYNC_WATCH_MS = 45 * 24 * 60 * 60_000\n\n/** The most pages of one search a run reads. */\nconst SEARCH_PAGES = 5\n/** Handled message ids a mailbox keeps, and an enrollment keeps. */\nconst MAILBOX_HANDLED_MAX = 500\nconst ENROLLMENT_HANDLED_MAX = 100\n/** Addresses in one `from:` search. */\nconst FROM_CHUNK = 15\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface OutreachSyncReport {\n [key: string]: number\n mailboxes: number\n threads: number\n replies: number\n optOuts: number\n bounces: number\n /** Hard bounces that read as a gateway block, each filing its domain (AGL-3244). */\n gatewayBlocks: number\n postponed: number\n paused: number\n}\n\ninterface SyncContext {\n deps: OutreachRuntimeDeps\n firestore: Firestore\n orgId: string\n mailbox: OutreachMailbox\n client: GmailClient\n selfAddresses: string[]\n unsubscribeAddress: string | null\n sequences: Map<string, OutreachSequence | null>\n delta: OutreachMailboxHealthDelta\n report: OutreachSyncReport\n nowMs: number\n}\n\n/** Runs the sync job once — see the module note. */\nexport async function runOutreachSyncJob(\n deps: OutreachRuntimeDeps,\n context: { nowMs: number; deadlineMs: number },\n): Promise<OutreachSyncReport> {\n const firestore = deps.firestore()\n const report: OutreachSyncReport = {\n mailboxes: 0,\n threads: 0,\n replies: 0,\n optOuts: 0,\n bounces: 0,\n gatewayBlocks: 0,\n postponed: 0,\n paused: 0,\n }\n const all = await firestore.collectionGroup(OUTREACH_COLLECTIONS.mailboxes).get()\n const byOrg = new Map<string, OutreachMailbox[]>()\n for (const doc of all.docs) {\n const orgId = doc.ref.parent.parent?.id\n const mailbox = readStoredOutreachMailbox(doc.id, doc.data())\n if (!orgId || !mailbox || (mailbox.status !== 'connected' && mailbox.status !== 'paused')) continue\n byOrg.set(orgId, [...(byOrg.get(orgId) ?? []), mailbox])\n }\n for (const [orgId, mailboxes] of byOrg) {\n if (deps.now() >= context.deadlineMs) break\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))) continue\n for (const mailbox of mailboxes) {\n if (deps.now() >= context.deadlineMs) break\n try {\n await syncMailbox(deps, firestore, { orgId, mailbox, report, nowMs: context.nowMs })\n report.mailboxes += 1\n } catch (error) {\n // One mailbox's failure is its own; the next mailbox is read. A grant\n // Google refused is the mailbox's to reconnect.\n if (isReconnectRequired(error)) {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: error instanceof GmailTransportError ? error.code : 'invalid_grant',\n nowMs: context.nowMs,\n })\n }\n console.error('[outreach] syncing a mailbox failed', error)\n }\n }\n }\n return report\n}\n\n/** Every stub a search finds, a few pages deep. */\nasync function search(\n client: GmailClient,\n q: string,\n): Promise<Array<{ id: string; threadId: string }>> {\n const found: Array<{ id: string; threadId: string }> = []\n let pageToken: string | null = null\n for (let page = 0; page < SEARCH_PAGES; page += 1) {\n const list = await client.listMessages({ q, maxResults: 100, pageToken, includeSpamTrash: true })\n found.push(...list.messages.filter((message) => message.id))\n pageToken = list.nextPageToken\n if (!pageToken) break\n }\n return found\n}\n\nasync function syncMailbox(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n input: { orgId: string; mailbox: OutreachMailbox; report: OutreachSyncReport; nowMs: number },\n): Promise<void> {\n const { orgId, mailbox, report, nowMs } = input\n const opened = await deps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n if (opened.reason !== 'not-configured') {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n }\n return\n }\n const unsubscribe = outreachUnsubscribeMailbox(mailbox.email)\n const context: SyncContext = {\n deps,\n firestore,\n orgId,\n mailbox,\n client: opened.client,\n selfAddresses: [\n mailbox.email,\n mailbox.sendAs,\n ...(mailbox.sendAsOptions ?? []).map((option) => option.email),\n ].filter(Boolean),\n unsubscribeAddress: unsubscribe?.address ?? null,\n sequences: new Map(),\n delta: emptyOutreachHealthDelta(),\n report,\n nowMs,\n }\n\n // The enrollments this mailbox is writing to, by thread and by address.\n const watched = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('mailboxId', '==', mailbox.id)\n .where('lastSentAtMs', '>=', nowMs - OUTREACH_SYNC_WATCH_MS)\n .get()\n const byThread = new Map<string, OutreachEnrollment>()\n const byAddress = new Map<string, OutreachEnrollment>()\n for (const doc of watched.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment) continue\n for (const threadId of enrollment.gmailThreadIds) byThread.set(threadId, enrollment)\n const known = byAddress.get(enrollment.email)\n if (!known || (enrollment.lastSentAtMs ?? 0) > (known.lastSentAtMs ?? 0)) byAddress.set(enrollment.email, enrollment)\n }\n\n const sinceMs = Math.max(Number(mailbox.sync?.throughMs) || 0, nowMs - OUTREACH_SYNC_LOOKBACK_MS) - OUTREACH_SYNC_OVERLAP_MS\n const after = `after:${Math.floor(sinceMs / 1000)}`\n // Every message this run or an earlier one handled outside an enrollment's\n // thread; one message is handled once however many searches find it.\n const handled = new Set(mailbox.sync?.handledMessageIds ?? [])\n const newlyHandled: string[] = []\n const markHandled = (id: string) => {\n if (handled.has(id)) return\n handled.add(id)\n newlyHandled.push(id)\n }\n const fresh = (stub: { id: string; threadId: string }) => !handled.has(stub.id) && !byThread.has(stub.threadId)\n\n // 1. The enrollments' own threads that have new mail.\n const inbound = await search(context.client, `${after} -from:me`)\n for (const threadId of new Set(inbound.map((stub) => stub.threadId))) {\n const enrollment = byThread.get(threadId)\n if (!enrollment) continue\n const thread = await context.client.getFullThread(threadId)\n report.threads += 1\n await applyMessages(context, enrollment, thread.messages.map(outreachThreadMessageFromGmail))\n }\n\n // 2. Delivery reports outside those threads: a receiving server's bounce.\n for (const stub of await search(context.client, `${after} from:(mailer-daemon OR postmaster)`)) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const delivery = readOutreachDeliveryReport(message)\n if (delivery?.kind === 'hard') {\n for (const address of delivery.failedAddresses) {\n const enrollment = byAddress.get(address)\n if (enrollment) await applyMessages(context, enrollment, [message])\n }\n }\n markHandled(stub.id)\n }\n\n // 3. Messages to the unsubscribe address: whoever wrote asked to leave.\n if (unsubscribe) {\n for (const stub of await search(context.client, `${after} to:${unsubscribe.address}`)) {\n if (handled.has(stub.id)) continue\n const metadata = await context.client.getMessage(stub.id, { metadataHeaders: ['From'] })\n const from = emailAddressOf(metadata.headers.find((header) => header.name.toLowerCase() === 'from')?.value ?? '')\n const address = normalizeContactEmail(from)\n if (address && !context.selfAddresses.includes(address)) {\n const enrollment = byAddress.get(address) ?? null\n await recordOutreachOptOut(deps, {\n orgId,\n email: address,\n source: 'unsubscribe',\n enrollment,\n hostIds: enrollment ? [enrollment.hostId] : [],\n detail: 'Wrote to the unsubscribe address.',\n nowMs,\n })\n report.optOuts += 1\n }\n markHandled(stub.id)\n }\n }\n\n // 4. The people this mailbox writes to, answering in a thread of their own.\n const addresses = [...byAddress.keys()]\n for (let start = 0; start < addresses.length; start += FROM_CHUNK) {\n const chunk = addresses.slice(start, start + FROM_CHUNK)\n for (const stub of await search(context.client, `${after} from:(${chunk.join(' OR ')})`)) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const enrollment = byAddress.get(normalizeContactEmail(emailAddressOf(message.from)) ?? '')\n if (enrollment) await applyMessages(context, enrollment, [message])\n markHandled(stub.id)\n }\n }\n\n const paused = await applyOutreachMailboxHealth(deps, { orgId, mailboxId: mailbox.id, delta: context.delta })\n if (paused) report.paused += 1\n await mailboxRef(firestore, orgId, mailbox.id).update({\n sync: {\n throughMs: nowMs,\n handledMessageIds: [...(mailbox.sync?.handledMessageIds ?? []), ...newlyHandled].slice(-MAILBOX_HANDLED_MAX),\n },\n })\n}\n\n/** The sequence an enrollment runs through, read once per run. */\nasync function sequenceOf(context: SyncContext, sequenceId: string): Promise<OutreachSequence | null> {\n if (!context.sequences.has(sequenceId)) {\n const snapshot = await outreachOrgCollection(context.firestore, context.orgId, 'sequences').doc(sequenceId).get()\n context.sequences.set(sequenceId, readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined))\n }\n return context.sequences.get(sequenceId) ?? null\n}\n\n/** A message's `Message-ID` in header form, or `null` when it carries none. */\nconst messageIdOf = (message: OutreachThreadMessage) => outreachMessageIdHeader(outreachHeader(message, 'Message-ID'))\n\n/** A bounce's diagnostic with the recipient's address taken out of it. */\nexport function scrubbedOutreachDiagnostic(diagnostic: string | null, email: string): string | null {\n if (!diagnostic) return null\n const pattern = new RegExp(email.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'gi')\n return diagnostic.replace(pattern, 'the address')\n}\n\n/** Files a message the person wrote on their timeline. */\nasync function fileInbound(context: SyncContext, enrollment: OutreachEnrollment, message: OutreachThreadMessage) {\n const messageId = messageIdOf(message)\n if (!messageId) return\n await fileOutreachEmail(context.deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n direction: 'inbound',\n subject: message.subject,\n from: emailAddressOf(message.from) ?? message.from,\n to: context.mailbox.sendAs || context.mailbox.email,\n messageId,\n inReplyTo: outreachMessageIdHeader(outreachHeader(message, 'In-Reply-To')),\n body: crmInboundExcerpt(message.textBody ?? message.snippet ?? '', message.htmlBody ?? ''),\n atMs: message.internalDateMs,\n byUid: '',\n })\n}\n\n/**\n * What the new messages of one enrollment mean, done: the effects first,\n * each idempotent, then the event and the handled ids in one transaction.\n */\nasync function applyMessages(\n context: SyncContext,\n enrollment: OutreachEnrollment,\n messages: readonly OutreachThreadMessage[],\n): Promise<void> {\n const { deps, firestore, report, nowMs } = context\n const decision: OutreachThreadDecision = decideOutreachThread({\n messages,\n recipient: enrollment.email,\n handledMessageIds: enrollment.syncedMessageIds ?? [],\n selfAddresses: context.selfAddresses,\n unsubscribeAddress: context.unsubscribeAddress,\n threadSubject: enrollment.threadSubject,\n })\n const freshIds = decision.classifications.map((classification) => classification.messageId).filter(Boolean)\n if (!freshIds.length) return\n const decidedBy: OutreachMessageClassification | null = decision.decidedBy\n const decidingMessage = decidedBy ? messages.find((message) => message.id === decidedBy.messageId) ?? null : null\n const lastSentMessageId = enrollment.messageIds[enrollment.messageIds.length - 1] ?? null\n\n for (const classification of decision.classifications) {\n const aboutRecipient =\n !classification.bounce?.recipients.length || classification.bounce.recipients.includes(enrollment.email)\n if (classification.kind === 'hard_bounce' && aboutRecipient) {\n context.delta.bounces.push({ atMs: classification.atMs || nowMs, messageId: lastSentMessageId })\n }\n if (classification.complaint) {\n context.delta.complaintAtMs = Math.max(context.delta.complaintAtMs ?? 0, classification.atMs || nowMs)\n }\n }\n\n let event: OutreachEnrollmentEvent | null = null\n let postponedTo: number | null = null\n switch (decision.outcome) {\n case 'opted_out': {\n if (decidingMessage) await fileInbound(context, enrollment, decidingMessage)\n const complaint = decidedBy?.complaint === true\n await recordOutreachOptOut(deps, {\n orgId: context.orgId,\n email: enrollment.email,\n source: decidedBy?.evidence?.startsWith('To:') ? 'unsubscribe' : 'opt_out_reply',\n enrollment,\n hostIds: [enrollment.hostId],\n detail: decidedBy?.evidence ?? null,\n nowMs,\n complaint,\n })\n // A reply that called the email spam marks the send it answered\n // (AGL-3245), as the campaign webhook would mark a reported one.\n if (complaint) {\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'complained',\n atMs: decidedBy?.atMs || nowMs,\n detail: decidedBy?.evidence ?? null,\n })\n }\n report.optOuts += 1\n break\n }\n case 'replied': {\n if (decidingMessage) {\n await fileInbound(context, enrollment, decidingMessage)\n try {\n await fileOutreachTask(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: `reply:${messageIdOf(decidingMessage) ?? decidingMessage.id}`,\n title: `Reply from ${enrollment.contactName || enrollment.email}`,\n notes: crmInboundExcerpt(decidingMessage.textBody ?? decidingMessage.snippet ?? '', decidingMessage.htmlBody ?? '').slice(0, 500),\n kind: 'email',\n dueAtMs: nowMs,\n assigneeUid: context.mailbox.connectedByUid,\n })\n } catch (error) {\n console.error('[outreach] the reply task could not be filed', error)\n }\n }\n // A lead somebody wrote back to is being worked (AGL-3234).\n if (enrollment.target === 'lead' && enrollment.leadId) {\n await markOutreachLeadWorking(firestore, { orgId: context.orgId, leadId: enrollment.leadId })\n }\n context.delta.replies += 1\n event = { type: 'reply', atMs: decidedBy?.atMs || nowMs, detail: decidedBy?.evidence ?? null }\n report.replies += 1\n // Once per enrollment (AGL-3254): a reply moves the status to\n // `replied`, and nothing is synced for it again.\n await creditOutreachReply(deps, { enrollment, atMs: event.atMs })\n break\n }\n case 'bounced': {\n const diagnostic = scrubbedOutreachDiagnostic(decidedBy?.bounce?.diagnostic ?? decidedBy?.bounce?.status ?? null, enrollment.email)\n await deps.suppressBouncedEmail({ email: enrollment.email, hostId: enrollment.hostId })\n await addOutreachDoNotContact(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n reason: 'hard_bounce',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n let detail = diagnostic\n // A gateway block is the domain's verdict on the sender (AGL-3244):\n // the domain is filed beside the address, and the enrollment says so.\n // Never a public mailbox provider's domain — Gmail refusing one\n // message on policy is not a reason to stop writing to Gmail.\n const domain = outreachEmailDomain(enrollment.email)\n const gateway = isOutreachGatewayBlock(decidedBy?.bounce ?? null)\n if (domain && !isPublicMailboxDomain(domain) && gateway) {\n await addOutreachDoNotContactDomain(firestore, {\n orgId: context.orgId,\n domain,\n reason: 'gateway_block',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n detail = outreachGatewayBlockDetail(domain, diagnostic)\n report.gatewayBlocks += 1\n }\n const bouncedAtMs = decidedBy?.atMs || nowMs\n // The record the person is says so too (AGL-3245): the verdict on the\n // lead and the contact, and the bounce on the send's own timeline\n // entry, so the record reads Sent, then Bounced.\n await deps.stampRecordEmailState({\n orgId: context.orgId,\n email: enrollment.email,\n state: {\n status: gateway ? 'blocked' : 'bounced',\n atMs: bouncedAtMs,\n source: 'sequence',\n detail: diagnostic,\n enrollmentId: enrollment.id,\n },\n })\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'bounced',\n atMs: bouncedAtMs,\n detail: diagnostic,\n })\n event = { type: 'bounce', atMs: bouncedAtMs, detail }\n report.bounces += 1\n break\n }\n case 'postpone': {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n postponedTo = postponeOutreachForAutoReply({\n nextDueAtMs: enrollment.nextDueAtMs,\n receivedAtMs: decidedBy?.atMs || nowMs,\n timeZone: context.mailbox.timezone,\n window: effectiveOutreachWindow(sequence?.settings.window, context.mailbox.window),\n random: deps.random,\n })\n break\n }\n default:\n break\n }\n\n const outcome = await applyOutreachEvent(firestore, {\n orgId: context.orgId,\n enrollmentId: enrollment.id,\n event,\n extra: (current) => {\n const extra: Record<string, unknown> = {\n syncedMessageIds: [...(current.syncedMessageIds ?? []), ...freshIds].slice(-ENROLLMENT_HANDLED_MAX),\n }\n const waiting = current.status === 'active' || current.status === 'paused'\n if (postponedTo !== null && waiting && current.nextDueAtMs !== null && postponedTo > current.nextDueAtMs) {\n extra['nextDueAtMs'] = postponedTo\n }\n return extra\n },\n nowMs,\n })\n if (postponedTo !== null && outcome.enrollment?.nextDueAtMs === postponedTo) report.postponed += 1\n // The person's record says the sequence ended (AGL-3274), once, under the\n // reply that ended it — after the transaction moved the status, so a run\n // that reads the same reply again finds nothing to file. An opt-out's\n // line is `recordOutreachOptOut`'s, beside the lists it writes.\n if (event?.type === 'reply' && outcome.changed) {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n const entry = outreachStoppedEntry({\n enrollmentId: enrollment.id,\n sequenceName: sequence?.name ?? '',\n reason: 'replied',\n })\n await fileOutreachNote(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: event.atMs,\n })\n }\n // The run's own view of the enrollment moves with it, so a second message\n // of it in this run is judged against what this one did.\n if (outcome.enrollment) Object.assign(enrollment, outcome.enrollment)\n}\n"],"names":["normalizeContactEmail","isPublicMailboxDomain","crmInboundExcerpt","emailAddressOf","outreachMessageIdHeader","readOutreachDeliveryReport","outreachEmailDomain","outreachStoppedEntry","isOutreachGatewayBlock","outreachGatewayBlockDetail","effectiveOutreachWindow","postponeOutreachForAutoReply","decideOutreachThread","outreachHeader","outreachThreadMessageFromGmail","mailboxRef","OUTREACH_COLLECTIONS","addOutreachDoNotContact","addOutreachDoNotContactDomain","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","creditOutreachReply","applyOutreachEvent","recordOutreachOptOut","applyOutreachMailboxHealth","emptyOutreachHealthDelta","noteOutreachMailboxReconnectRequired","markOutreachLeadWorking","fileOutreachEmail","fileOutreachNote","fileOutreachTask","markOutreachEmailDelivery","outreachUnsubscribeMailbox","OUTREACH_SYNC_JOB_ID","OUTREACH_SYNC_LOOKBACK_MS","OUTREACH_SYNC_OVERLAP_MS","OUTREACH_SYNC_WATCH_MS","SEARCH_PAGES","MAILBOX_HANDLED_MAX","ENROLLMENT_HANDLED_MAX","FROM_CHUNK","runOutreachSyncJob","deps","context","firestore","report","mailboxes","threads","replies","optOuts","bounces","gatewayBlocks","postponed","paused","all","collectionGroup","get","byOrg","Map","doc","docs","orgId","ref","parent","id","mailbox","data","status","set","now","deadlineMs","orgSnapshot","collection","org","exists","orgRefusal","syncMailbox","nowMs","error","mailboxId","errorCode","code","console","search","client","q","found","pageToken","page","list","listMessages","maxResults","includeSpamTrash","push","messages","filter","message","nextPageToken","input","opened","openMailbox","ok","reason","unsubscribe","email","selfAddresses","sendAs","sendAsOptions","map","option","Boolean","unsubscribeAddress","address","sequences","delta","watched","where","byThread","byAddress","enrollment","known","threadId","gmailThreadIds","lastSentAtMs","sinceMs","Math","max","Number","sync","throughMs","after","floor","handled","Set","handledMessageIds","newlyHandled","markHandled","has","add","fresh","stub","inbound","thread","getFullThread","applyMessages","getFullMessage","delivery","kind","failedAddresses","metadata","getMessage","metadataHeaders","from","headers","find","header","name","toLowerCase","value","includes","source","hostIds","hostId","detail","addresses","keys","start","length","chunk","slice","join","update","sequenceOf","sequenceId","snapshot","undefined","messageIdOf","scrubbedOutreachDiagnostic","diagnostic","pattern","RegExp","replace","fileInbound","messageId","link","direction","subject","to","inReplyTo","body","textBody","snippet","htmlBody","atMs","internalDateMs","byUid","outcome","decision","recipient","syncedMessageIds","threadSubject","freshIds","classifications","classification","decidedBy","decidingMessage","lastSentMessageId","messageIds","aboutRecipient","bounce","recipients","complaint","complaintAtMs","event","postponedTo","evidence","startsWith","state","dedupeKey","title","contactName","notes","dueAtMs","assigneeUid","connectedByUid","target","leadId","type","suppressBouncedEmail","enrollmentId","domain","gateway","bouncedAtMs","stampRecordEmailState","sequence","nextDueAtMs","receivedAtMs","timeZone","timezone","window","settings","random","extra","current","waiting","changed","entry","sequenceName","Object","assign"],"mappings":"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"}
|
|
@@ -50,6 +50,20 @@ export declare function fileOutreachEmail(deps: Pick<OutreachRuntimeDeps, 'timel
|
|
|
50
50
|
byUid: string;
|
|
51
51
|
byName?: string | null;
|
|
52
52
|
}): Promise<PluginRecordWrite | null>;
|
|
53
|
+
/**
|
|
54
|
+
* Files one of the runtime's own lines on a person's record (AGL-3274) —
|
|
55
|
+
* "Enrolled in", "stopped" — as a note by "Sequences", once per the key
|
|
56
|
+
* given; never throws. The words and the keys are `enrollment-activity.ts`'s.
|
|
57
|
+
*/
|
|
58
|
+
export declare function fileOutreachNote(deps: Pick<OutreachRuntimeDeps, 'timeline'>, input: {
|
|
59
|
+
orgId: string;
|
|
60
|
+
hostId: string;
|
|
61
|
+
/** The record the entry lands on: `{ contactId }` or `{ leadId }`. */
|
|
62
|
+
link: PluginRecordLink;
|
|
63
|
+
dedupeKey: string;
|
|
64
|
+
body: string;
|
|
65
|
+
atMs: number;
|
|
66
|
+
}): Promise<PluginRecordWrite | null>;
|
|
53
67
|
/**
|
|
54
68
|
* Marks a sent email bounced or reported on the record it was filed on
|
|
55
69
|
* (AGL-3245), so the timeline reads Sent, then Bounced, with what the
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
17
|
import { OUTREACH_PLUGIN_ID } from "../constants/bundle-common.js";
|
|
18
|
+
import { OUTREACH_TIMELINE_BY_NAME } from "../engine/enrollment-activity.js";
|
|
18
19
|
/**
|
|
19
20
|
* OUTREACH ON THE CONTACT'S TIMELINE (AGL-2981).
|
|
20
21
|
*
|
|
@@ -64,6 +65,33 @@ import { OUTREACH_PLUGIN_ID } from "../constants/bundle-common.js";
|
|
|
64
65
|
return null;
|
|
65
66
|
}
|
|
66
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Files one of the runtime's own lines on a person's record (AGL-3274) —
|
|
70
|
+
* "Enrolled in", "stopped" — as a note by "Sequences", once per the key
|
|
71
|
+
* given; never throws. The words and the keys are `enrollment-activity.ts`'s.
|
|
72
|
+
*/ export async function fileOutreachNote(deps, input) {
|
|
73
|
+
const writer = deps.timeline();
|
|
74
|
+
if (!writer) return null;
|
|
75
|
+
try {
|
|
76
|
+
const written = await writer.logActivity({
|
|
77
|
+
orgId: input.orgId,
|
|
78
|
+
hostId: input.hostId,
|
|
79
|
+
link: input.link,
|
|
80
|
+
sourcePluginId: OUTREACH_PLUGIN_ID,
|
|
81
|
+
kind: 'note',
|
|
82
|
+
atMs: input.atMs,
|
|
83
|
+
body: input.body,
|
|
84
|
+
byUid: '',
|
|
85
|
+
byName: OUTREACH_TIMELINE_BY_NAME,
|
|
86
|
+
dedupeKey: input.dedupeKey
|
|
87
|
+
});
|
|
88
|
+
if (written.ok === false) console.warn(`[outreach] the record system did not file a note: ${written.error}`);
|
|
89
|
+
return written;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('[outreach] filing a note on the record failed', error);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
67
95
|
/**
|
|
68
96
|
* Marks a sent email bounced or reported on the record it was filed on
|
|
69
97
|
* (AGL-3245), so the timeline reads Sent, then Bounced, with what the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/timeline.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 {\n PluginRecordDeliveryRequest,\n PluginRecordLink,\n PluginRecordTaskKind,\n PluginRecordWrite,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { OUTREACH_PLUGIN_ID } from '../constants/bundle-common'\nimport type { OutreachTaskKind } from '../model/outreach.types'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\n\n/**\n * OUTREACH ON THE CONTACT'S TIMELINE (AGL-2981).\n *\n * Every email a sequence sends, every reply it reads, and every task it\n * asks of the rep goes onto the contact's record through the core's\n * record-timeline seam — whichever plugin keeps the workspace's records —\n * never by writing that plugin's documents. An email is filed once per\n * `Message-ID`, under the id the capture address would file a forwarded copy\n * by; a task once per the key given here.\n *\n * The timeline is bookkeeping beside the act: an email has already left\n * when it is filed, so a writer that refuses or fails is logged and the\n * send stands. A workspace with no record system files nothing.\n */\n\n/** The record system's word for a step's task kind. */\nexport const OUTREACH_TASK_KIND_TO_RECORD: Record<OutreachTaskKind, PluginRecordTaskKind> = {\n call: 'call',\n linkedin: 'todo',\n todo: 'todo',\n}\n\n/** Files one email on a person's record — the contact, or the lead while they are one (AGL-3234); never throws. */\nexport async function fileOutreachEmail(\n deps: Pick<OutreachRuntimeDeps, 'timeline'>,\n input: {\n orgId: string\n hostId: string\n /** The record the entry lands on: `{ contactId }` or `{ leadId }`. */\n link: PluginRecordLink\n direction: 'outbound' | 'inbound'\n subject: string\n from: string\n to: string | null\n messageId: string\n inReplyTo?: string | null\n body: string\n atMs: number\n byUid: string\n byName?: string | null\n },\n): Promise<PluginRecordWrite | null> {\n const writer = deps.timeline()\n if (!writer) return null\n try {\n const written = await writer.logActivity({\n orgId: input.orgId,\n hostId: input.hostId,\n link: input.link,\n sourcePluginId: OUTREACH_PLUGIN_ID,\n kind: 'email',\n atMs: input.atMs,\n body: input.body,\n byUid: input.byUid,\n byName: input.byName ?? null,\n email: {\n direction: input.direction,\n subject: input.subject,\n from: input.from,\n to: input.to,\n messageId: input.messageId,\n inReplyTo: input.inReplyTo ?? null,\n },\n })\n if (written.ok === false) console.warn(`[outreach] the record system did not file an email: ${written.error}`)\n return written\n } catch (error) {\n console.error('[outreach] filing an email on the record failed', error)\n return null\n }\n}\n\n/**\n * Marks a sent email bounced or reported on the record it was filed on\n * (AGL-3245), so the timeline reads Sent, then Bounced, with what the\n * server said. Answers `null` when the workspace keeps no records or its\n * record system keeps no delivery state; never throws.\n */\nexport async function markOutreachEmailDelivery(\n deps: Pick<OutreachRuntimeDeps, 'timeline'>,\n input: Omit<PluginRecordDeliveryRequest, 'messageId'> & { messageId: string | null },\n): Promise<PluginRecordWrite | null> {\n const writer = deps.timeline()\n if (!writer?.recordEmailDelivery || !input.messageId) return null\n try {\n const written = await writer.recordEmailDelivery({ ...input, messageId: input.messageId })\n if (written.ok === false && written.status !== 404) {\n console.warn(`[outreach] the record system did not mark the email ${input.state}: ${written.error}`)\n }\n return written\n } catch (error) {\n console.error('[outreach] marking an email on the record failed', error)\n return null\n }\n}\n\n/**\n * Files one task on a contact. Answers the record system's answer, `null`\n * when the workspace keeps no records; a writer that THROWS is the caller's,\n * which a step waits out rather than completing without its task.\n */\nexport async function fileOutreachTask(\n deps: Pick<OutreachRuntimeDeps, 'timeline'>,\n input: {\n orgId: string\n hostId: string\n /** The record the task lands on: `{ contactId }` or `{ leadId }`. */\n link: PluginRecordLink\n dedupeKey: string\n title: string\n notes?: string | null\n kind: PluginRecordTaskKind\n dueAtMs: number\n assigneeUid: string\n },\n): Promise<PluginRecordWrite | null> {\n const writer = deps.timeline()\n if (!writer) return null\n return writer.createTask({\n orgId: input.orgId,\n hostId: input.hostId,\n link: input.link,\n sourcePluginId: OUTREACH_PLUGIN_ID,\n dedupeKey: input.dedupeKey,\n title: input.title,\n notes: input.notes ?? null,\n kind: input.kind,\n dueAtMs: input.dueAtMs,\n assigneeUid: input.assigneeUid,\n createdByUid: '',\n })\n}\n"],"names":["OUTREACH_PLUGIN_ID","OUTREACH_TASK_KIND_TO_RECORD","call","linkedin","todo","fileOutreachEmail","deps","input","writer","timeline","written","logActivity","orgId","hostId","link","sourcePluginId","kind","atMs","body","byUid","byName","email","direction","subject","from","to","messageId","inReplyTo","ok","console","warn","error","markOutreachEmailDelivery","recordEmailDelivery","status","state","fileOutreachTask","createTask","dedupeKey","title","notes","dueAtMs","assigneeUid","createdByUid"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAQD,SAASA,kBAAkB,QAAQ,gCAA4B;AAI/D;;;;;;;;;;;;;CAaC,GAED,qDAAqD,GACrD,OAAO,MAAMC,+BAA+E;IAC1FC,MAAM;IACNC,UAAU;IACVC,MAAM;AACR,EAAC;AAED,iHAAiH,GACjH,OAAO,eAAeC,kBACpBC,IAA2C,EAC3CC,KAeC;IAED,MAAMC,SAASF,KAAKG,QAAQ;IAC5B,IAAI,CAACD,QAAQ,OAAO;IACpB,IAAI;YAUQD,eAOKA;QAhBf,MAAMG,UAAU,MAAMF,OAAOG,WAAW,CAAC;YACvCC,OAAOL,MAAMK,KAAK;YAClBC,QAAQN,MAAMM,MAAM;YACpBC,MAAMP,MAAMO,IAAI;YAChBC,gBAAgBf;YAChBgB,MAAM;YACNC,MAAMV,MAAMU,IAAI;YAChBC,MAAMX,MAAMW,IAAI;YAChBC,OAAOZ,MAAMY,KAAK;YAClBC,MAAM,GAAEb,gBAAAA,MAAMa,MAAM,YAAZb,gBAAgB;YACxBc,OAAO;gBACLC,WAAWf,MAAMe,SAAS;gBAC1BC,SAAShB,MAAMgB,OAAO;gBACtBC,MAAMjB,MAAMiB,IAAI;gBAChBC,IAAIlB,MAAMkB,EAAE;gBACZC,WAAWnB,MAAMmB,SAAS;gBAC1BC,SAAS,GAAEpB,mBAAAA,MAAMoB,SAAS,YAAfpB,mBAAmB;YAChC;QACF;QACA,IAAIG,QAAQkB,EAAE,KAAK,OAAOC,QAAQC,IAAI,CAAC,CAAC,oDAAoD,EAAEpB,QAAQqB,KAAK,EAAE;QAC7G,OAAOrB;IACT,EAAE,OAAOqB,OAAO;QACdF,QAAQE,KAAK,CAAC,mDAAmDA;QACjE,OAAO;IACT;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeC,0BACpB1B,IAA2C,EAC3CC,KAAoF;IAEpF,MAAMC,SAASF,KAAKG,QAAQ;IAC5B,IAAI,EAACD,0BAAAA,OAAQyB,mBAAmB,KAAI,CAAC1B,MAAMmB,SAAS,EAAE,OAAO;IAC7D,IAAI;QACF,MAAMhB,UAAU,MAAMF,OAAOyB,mBAAmB,CAAC,aAAK1B;YAAOmB,WAAWnB,MAAMmB,SAAS;;QACvF,IAAIhB,QAAQkB,EAAE,KAAK,SAASlB,QAAQwB,MAAM,KAAK,KAAK;YAClDL,QAAQC,IAAI,CAAC,CAAC,oDAAoD,EAAEvB,MAAM4B,KAAK,CAAC,EAAE,EAAEzB,QAAQqB,KAAK,EAAE;QACrG;QACA,OAAOrB;IACT,EAAE,OAAOqB,OAAO;QACdF,QAAQE,KAAK,CAAC,oDAAoDA;QAClE,OAAO;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeK,iBACpB9B,IAA2C,EAC3CC,KAWC;QAWQA;IATT,MAAMC,SAASF,KAAKG,QAAQ;IAC5B,IAAI,CAACD,QAAQ,OAAO;IACpB,OAAOA,OAAO6B,UAAU,CAAC;QACvBzB,OAAOL,MAAMK,KAAK;QAClBC,QAAQN,MAAMM,MAAM;QACpBC,MAAMP,MAAMO,IAAI;QAChBC,gBAAgBf;QAChBsC,WAAW/B,MAAM+B,SAAS;QAC1BC,OAAOhC,MAAMgC,KAAK;QAClBC,KAAK,GAAEjC,eAAAA,MAAMiC,KAAK,YAAXjC,eAAe;QACtBS,MAAMT,MAAMS,IAAI;QAChByB,SAASlC,MAAMkC,OAAO;QACtBC,aAAanC,MAAMmC,WAAW;QAC9BC,cAAc;IAChB;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/timeline.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 {\n PluginRecordDeliveryRequest,\n PluginRecordLink,\n PluginRecordTaskKind,\n PluginRecordWrite,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { OUTREACH_PLUGIN_ID } from '../constants/bundle-common'\nimport { OUTREACH_TIMELINE_BY_NAME } from '../engine/enrollment-activity'\nimport type { OutreachTaskKind } from '../model/outreach.types'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\n\n/**\n * OUTREACH ON THE CONTACT'S TIMELINE (AGL-2981).\n *\n * Every email a sequence sends, every reply it reads, and every task it\n * asks of the rep goes onto the contact's record through the core's\n * record-timeline seam — whichever plugin keeps the workspace's records —\n * never by writing that plugin's documents. An email is filed once per\n * `Message-ID`, under the id the capture address would file a forwarded copy\n * by; a task once per the key given here.\n *\n * The timeline is bookkeeping beside the act: an email has already left\n * when it is filed, so a writer that refuses or fails is logged and the\n * send stands. A workspace with no record system files nothing.\n */\n\n/** The record system's word for a step's task kind. */\nexport const OUTREACH_TASK_KIND_TO_RECORD: Record<OutreachTaskKind, PluginRecordTaskKind> = {\n call: 'call',\n linkedin: 'todo',\n todo: 'todo',\n}\n\n/** Files one email on a person's record — the contact, or the lead while they are one (AGL-3234); never throws. */\nexport async function fileOutreachEmail(\n deps: Pick<OutreachRuntimeDeps, 'timeline'>,\n input: {\n orgId: string\n hostId: string\n /** The record the entry lands on: `{ contactId }` or `{ leadId }`. */\n link: PluginRecordLink\n direction: 'outbound' | 'inbound'\n subject: string\n from: string\n to: string | null\n messageId: string\n inReplyTo?: string | null\n body: string\n atMs: number\n byUid: string\n byName?: string | null\n },\n): Promise<PluginRecordWrite | null> {\n const writer = deps.timeline()\n if (!writer) return null\n try {\n const written = await writer.logActivity({\n orgId: input.orgId,\n hostId: input.hostId,\n link: input.link,\n sourcePluginId: OUTREACH_PLUGIN_ID,\n kind: 'email',\n atMs: input.atMs,\n body: input.body,\n byUid: input.byUid,\n byName: input.byName ?? null,\n email: {\n direction: input.direction,\n subject: input.subject,\n from: input.from,\n to: input.to,\n messageId: input.messageId,\n inReplyTo: input.inReplyTo ?? null,\n },\n })\n if (written.ok === false) console.warn(`[outreach] the record system did not file an email: ${written.error}`)\n return written\n } catch (error) {\n console.error('[outreach] filing an email on the record failed', error)\n return null\n }\n}\n\n/**\n * Files one of the runtime's own lines on a person's record (AGL-3274) —\n * \"Enrolled in\", \"stopped\" — as a note by \"Sequences\", once per the key\n * given; never throws. The words and the keys are `enrollment-activity.ts`'s.\n */\nexport async function fileOutreachNote(\n deps: Pick<OutreachRuntimeDeps, 'timeline'>,\n input: {\n orgId: string\n hostId: string\n /** The record the entry lands on: `{ contactId }` or `{ leadId }`. */\n link: PluginRecordLink\n dedupeKey: string\n body: string\n atMs: number\n },\n): Promise<PluginRecordWrite | null> {\n const writer = deps.timeline()\n if (!writer) return null\n try {\n const written = await writer.logActivity({\n orgId: input.orgId,\n hostId: input.hostId,\n link: input.link,\n sourcePluginId: OUTREACH_PLUGIN_ID,\n kind: 'note',\n atMs: input.atMs,\n body: input.body,\n byUid: '',\n byName: OUTREACH_TIMELINE_BY_NAME,\n dedupeKey: input.dedupeKey,\n })\n if (written.ok === false) console.warn(`[outreach] the record system did not file a note: ${written.error}`)\n return written\n } catch (error) {\n console.error('[outreach] filing a note on the record failed', error)\n return null\n }\n}\n\n/**\n * Marks a sent email bounced or reported on the record it was filed on\n * (AGL-3245), so the timeline reads Sent, then Bounced, with what the\n * server said. Answers `null` when the workspace keeps no records or its\n * record system keeps no delivery state; never throws.\n */\nexport async function markOutreachEmailDelivery(\n deps: Pick<OutreachRuntimeDeps, 'timeline'>,\n input: Omit<PluginRecordDeliveryRequest, 'messageId'> & { messageId: string | null },\n): Promise<PluginRecordWrite | null> {\n const writer = deps.timeline()\n if (!writer?.recordEmailDelivery || !input.messageId) return null\n try {\n const written = await writer.recordEmailDelivery({ ...input, messageId: input.messageId })\n if (written.ok === false && written.status !== 404) {\n console.warn(`[outreach] the record system did not mark the email ${input.state}: ${written.error}`)\n }\n return written\n } catch (error) {\n console.error('[outreach] marking an email on the record failed', error)\n return null\n }\n}\n\n/**\n * Files one task on a contact. Answers the record system's answer, `null`\n * when the workspace keeps no records; a writer that THROWS is the caller's,\n * which a step waits out rather than completing without its task.\n */\nexport async function fileOutreachTask(\n deps: Pick<OutreachRuntimeDeps, 'timeline'>,\n input: {\n orgId: string\n hostId: string\n /** The record the task lands on: `{ contactId }` or `{ leadId }`. */\n link: PluginRecordLink\n dedupeKey: string\n title: string\n notes?: string | null\n kind: PluginRecordTaskKind\n dueAtMs: number\n assigneeUid: string\n },\n): Promise<PluginRecordWrite | null> {\n const writer = deps.timeline()\n if (!writer) return null\n return writer.createTask({\n orgId: input.orgId,\n hostId: input.hostId,\n link: input.link,\n sourcePluginId: OUTREACH_PLUGIN_ID,\n dedupeKey: input.dedupeKey,\n title: input.title,\n notes: input.notes ?? null,\n kind: input.kind,\n dueAtMs: input.dueAtMs,\n assigneeUid: input.assigneeUid,\n createdByUid: '',\n })\n}\n"],"names":["OUTREACH_PLUGIN_ID","OUTREACH_TIMELINE_BY_NAME","OUTREACH_TASK_KIND_TO_RECORD","call","linkedin","todo","fileOutreachEmail","deps","input","writer","timeline","written","logActivity","orgId","hostId","link","sourcePluginId","kind","atMs","body","byUid","byName","email","direction","subject","from","to","messageId","inReplyTo","ok","console","warn","error","fileOutreachNote","dedupeKey","markOutreachEmailDelivery","recordEmailDelivery","status","state","fileOutreachTask","createTask","title","notes","dueAtMs","assigneeUid","createdByUid"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAQD,SAASA,kBAAkB,QAAQ,gCAA4B;AAC/D,SAASC,yBAAyB,QAAQ,mCAA+B;AAIzE;;;;;;;;;;;;;CAaC,GAED,qDAAqD,GACrD,OAAO,MAAMC,+BAA+E;IAC1FC,MAAM;IACNC,UAAU;IACVC,MAAM;AACR,EAAC;AAED,iHAAiH,GACjH,OAAO,eAAeC,kBACpBC,IAA2C,EAC3CC,KAeC;IAED,MAAMC,SAASF,KAAKG,QAAQ;IAC5B,IAAI,CAACD,QAAQ,OAAO;IACpB,IAAI;YAUQD,eAOKA;QAhBf,MAAMG,UAAU,MAAMF,OAAOG,WAAW,CAAC;YACvCC,OAAOL,MAAMK,KAAK;YAClBC,QAAQN,MAAMM,MAAM;YACpBC,MAAMP,MAAMO,IAAI;YAChBC,gBAAgBhB;YAChBiB,MAAM;YACNC,MAAMV,MAAMU,IAAI;YAChBC,MAAMX,MAAMW,IAAI;YAChBC,OAAOZ,MAAMY,KAAK;YAClBC,MAAM,GAAEb,gBAAAA,MAAMa,MAAM,YAAZb,gBAAgB;YACxBc,OAAO;gBACLC,WAAWf,MAAMe,SAAS;gBAC1BC,SAAShB,MAAMgB,OAAO;gBACtBC,MAAMjB,MAAMiB,IAAI;gBAChBC,IAAIlB,MAAMkB,EAAE;gBACZC,WAAWnB,MAAMmB,SAAS;gBAC1BC,SAAS,GAAEpB,mBAAAA,MAAMoB,SAAS,YAAfpB,mBAAmB;YAChC;QACF;QACA,IAAIG,QAAQkB,EAAE,KAAK,OAAOC,QAAQC,IAAI,CAAC,CAAC,oDAAoD,EAAEpB,QAAQqB,KAAK,EAAE;QAC7G,OAAOrB;IACT,EAAE,OAAOqB,OAAO;QACdF,QAAQE,KAAK,CAAC,mDAAmDA;QACjE,OAAO;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeC,iBACpB1B,IAA2C,EAC3CC,KAQC;IAED,MAAMC,SAASF,KAAKG,QAAQ;IAC5B,IAAI,CAACD,QAAQ,OAAO;IACpB,IAAI;QACF,MAAME,UAAU,MAAMF,OAAOG,WAAW,CAAC;YACvCC,OAAOL,MAAMK,KAAK;YAClBC,QAAQN,MAAMM,MAAM;YACpBC,MAAMP,MAAMO,IAAI;YAChBC,gBAAgBhB;YAChBiB,MAAM;YACNC,MAAMV,MAAMU,IAAI;YAChBC,MAAMX,MAAMW,IAAI;YAChBC,OAAO;YACPC,QAAQpB;YACRiC,WAAW1B,MAAM0B,SAAS;QAC5B;QACA,IAAIvB,QAAQkB,EAAE,KAAK,OAAOC,QAAQC,IAAI,CAAC,CAAC,kDAAkD,EAAEpB,QAAQqB,KAAK,EAAE;QAC3G,OAAOrB;IACT,EAAE,OAAOqB,OAAO;QACdF,QAAQE,KAAK,CAAC,iDAAiDA;QAC/D,OAAO;IACT;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeG,0BACpB5B,IAA2C,EAC3CC,KAAoF;IAEpF,MAAMC,SAASF,KAAKG,QAAQ;IAC5B,IAAI,EAACD,0BAAAA,OAAQ2B,mBAAmB,KAAI,CAAC5B,MAAMmB,SAAS,EAAE,OAAO;IAC7D,IAAI;QACF,MAAMhB,UAAU,MAAMF,OAAO2B,mBAAmB,CAAC,aAAK5B;YAAOmB,WAAWnB,MAAMmB,SAAS;;QACvF,IAAIhB,QAAQkB,EAAE,KAAK,SAASlB,QAAQ0B,MAAM,KAAK,KAAK;YAClDP,QAAQC,IAAI,CAAC,CAAC,oDAAoD,EAAEvB,MAAM8B,KAAK,CAAC,EAAE,EAAE3B,QAAQqB,KAAK,EAAE;QACrG;QACA,OAAOrB;IACT,EAAE,OAAOqB,OAAO;QACdF,QAAQE,KAAK,CAAC,oDAAoDA;QAClE,OAAO;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeO,iBACpBhC,IAA2C,EAC3CC,KAWC;QAWQA;IATT,MAAMC,SAASF,KAAKG,QAAQ;IAC5B,IAAI,CAACD,QAAQ,OAAO;IACpB,OAAOA,OAAO+B,UAAU,CAAC;QACvB3B,OAAOL,MAAMK,KAAK;QAClBC,QAAQN,MAAMM,MAAM;QACpBC,MAAMP,MAAMO,IAAI;QAChBC,gBAAgBhB;QAChBkC,WAAW1B,MAAM0B,SAAS;QAC1BO,OAAOjC,MAAMiC,KAAK;QAClBC,KAAK,GAAElC,eAAAA,MAAMkC,KAAK,YAAXlC,eAAe;QACtBS,MAAMT,MAAMS,IAAI;QAChB0B,SAASnC,MAAMmC,OAAO;QACtBC,aAAapC,MAAMoC,WAAW;QAC9BC,cAAc;IAChB;AACF"}
|
|
@@ -21,4 +21,4 @@ export declare function outreachUnsubscribePage(input: {
|
|
|
21
21
|
title: string;
|
|
22
22
|
body: string;
|
|
23
23
|
}, status?: number): Response;
|
|
24
|
-
export declare function createOutreachUnsubscribeRoute(deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now' | 'optOutOfSalesTopic' | 'stampRecordEmailState'>): PluginWebApiHandler;
|
|
24
|
+
export declare function createOutreachUnsubscribeRoute(deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now' | 'optOutOfSalesTopic' | 'stampRecordEmailState' | 'timeline'>): PluginWebApiHandler;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/unsubscribe-route.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 { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport { outreachOrgCollection, readStoredOutreachEnrollment } from '../storage/outreach-records'\nimport { recordOutreachOptOut } from './enrollment-events'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { readOutreachUnsubscribeToken } from './unsubscribe-link'\n\n/**\n * ONE-CLICK UNSUBSCRIBE (AGL-2981): `GET` and `POST /api/outreach/unsubscribe?t=…`.\n *\n * The link in every Outreach email's `List-Unsubscribe` header. A mailbox\n * provider POSTs it with `List-Unsubscribe=One-Click` and no person present\n * (RFC 8058); a mail client that opens it for a person sends a GET. BOTH\n * act, and both answer a plain page saying it is done.\n *\n * That GET acts is deliberate, and the opposite of the platform's newsletter\n * links. A person who used their mail client's Unsubscribe command has asked\n * already, and a second button would be a way out with a hurdle in it. The\n * one caller that could fetch the link without asking — a link scanner — can\n * only make a cold sequence stop writing to somebody, which is the direction\n * cold email should err in.\n *\n * No sign-in: the signature in the token is the authority, and a token that\n * does not verify changes nothing. It is registered as a recipient link, so\n * it answers whether or not Outreach is released to the organization now —\n * an opt-out outlives a paused rollout. Each act is idempotent: the link\n * used twice changes nothing the first use did not.\n *\n * What it records is what a reply saying \"stop\" records: the organization's\n * do-not-contact list, the site's `sales` topic, and every open enrollment\n * of the address stopped. An enrollment that no longer exists belongs to a\n * person or a workspace already erased, whose every site already refuses\n * them; the page says the same thing either way, and reveals nothing.\n */\n\nconst PAGE_HEADERS = {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Cache-Control': 'no-store',\n 'X-Robots-Tag': 'noindex',\n 'Referrer-Policy': 'no-referrer',\n}\n\nconst escapeHtml = (value: string) =>\n value.replace(/[&<>\"']/g, (character) => `&#${character.charCodeAt(0)};`)\n\n/** The plain page both methods answer with. */\nexport function outreachUnsubscribePage(input: { title: string; body: string }, status = 200): Response {\n const html =\n '<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">' +\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' +\n // The reader's own colors, light or dark: the page has no theme to take.\n '<meta name=\"color-scheme\" content=\"light dark\">' +\n `<title>${escapeHtml(input.title)}</title>` +\n '<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;line-height:1.5}h1{font-size:1.5rem}</style>' +\n `</head><body><main><h1>${escapeHtml(input.title)}</h1><p>${escapeHtml(input.body)}</p></main></body></html>`\n return new Response(html, { status, headers: PAGE_HEADERS })\n}\n\nexport function createOutreachUnsubscribeRoute(\n deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now' | 'optOutOfSalesTopic' | 'stampRecordEmailState'>,\n): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'GET' && request.method !== 'POST') {\n return new Response('Method not allowed', { status: 405, headers: { Allow: 'GET, POST' } })\n }\n const target = readOutreachUnsubscribeToken(new URL(request.url).searchParams.get('t'))\n if (!target) {\n return outreachUnsubscribePage(\n {\n title: 'This link doesn’t work',\n body: 'This unsubscribe link isn’t valid. To stop these emails, reply to one of them with “no”.',\n },\n 400,\n )\n }\n const firestore = deps.firestore()\n const snapshot = await outreachOrgCollection(firestore, target.orgId, 'enrollments').doc(target.enrollmentId).get()\n const enrollment = readStoredOutreachEnrollment(target.enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (enrollment) {\n await recordOutreachOptOut(deps, {\n orgId: target.orgId,\n email: enrollment.email,\n source: 'unsubscribe',\n enrollment,\n hostIds: [enrollment.hostId],\n detail: request.method === 'POST' ? 'One-click unsubscribe.' : 'Used the unsubscribe link.',\n nowMs: deps.now(),\n })\n }\n const settings = await readOutreachComplianceSettingsDoc(firestore, target.orgId).catch(() => null)\n const sender = settings?.brandName || settings?.legalName || 'The sender'\n return outreachUnsubscribePage({\n title: 'You’re unsubscribed',\n body: `${sender} won’t send you any more of these emails.`,\n })\n }\n}\n"],"names":["readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachEnrollment","recordOutreachOptOut","readOutreachUnsubscribeToken","PAGE_HEADERS","escapeHtml","value","replace","character","charCodeAt","outreachUnsubscribePage","input","status","html","title","body","Response","headers","createOutreachUnsubscribeRoute","deps","request","method","Allow","target","URL","url","searchParams","get","firestore","snapshot","orgId","doc","enrollmentId","enrollment","exists","data","undefined","email","source","hostIds","hostId","detail","nowMs","now","settings","catch","sender","brandName","legalName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,iCAAiC,QAAQ,0CAAsC;AACxF,SAASC,qBAAqB,EAAEC,4BAA4B,QAAQ,iCAA6B;AACjG,SAASC,oBAAoB,QAAQ,yBAAqB;AAE1D,SAASC,4BAA4B,QAAQ,wBAAoB;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GAED,MAAMC,eAAe;IACnB,gBAAgB;IAChB,iBAAiB;IACjB,gBAAgB;IAChB,mBAAmB;AACrB;AAEA,MAAMC,aAAa,CAACC,QAClBA,MAAMC,OAAO,CAAC,YAAY,CAACC,YAAc,CAAC,EAAE,EAAEA,UAAUC,UAAU,CAAC,GAAG,CAAC,CAAC;AAE1E,6CAA6C,GAC7C,OAAO,SAASC,wBAAwBC,KAAsC,EAAEC,SAAS,GAAG;IAC1F,MAAMC,OACJ,gEACA,yEACA,yEAAyE;IACzE,oDACA,CAAC,OAAO,EAAER,WAAWM,MAAMG,KAAK,EAAE,QAAQ,CAAC,GAC3C,8IACA,CAAC,uBAAuB,EAAET,WAAWM,MAAMG,KAAK,EAAE,QAAQ,EAAET,WAAWM,MAAMI,IAAI,EAAE,yBAAyB,CAAC;IAC/G,OAAO,IAAIC,SAASH,MAAM;QAAED;QAAQK,SAASb;IAAa;AAC5D;AAEA,OAAO,SAASc,+BACdC,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/unsubscribe-route.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 { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport { outreachOrgCollection, readStoredOutreachEnrollment } from '../storage/outreach-records'\nimport { recordOutreachOptOut } from './enrollment-events'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { readOutreachUnsubscribeToken } from './unsubscribe-link'\n\n/**\n * ONE-CLICK UNSUBSCRIBE (AGL-2981): `GET` and `POST /api/outreach/unsubscribe?t=…`.\n *\n * The link in every Outreach email's `List-Unsubscribe` header. A mailbox\n * provider POSTs it with `List-Unsubscribe=One-Click` and no person present\n * (RFC 8058); a mail client that opens it for a person sends a GET. BOTH\n * act, and both answer a plain page saying it is done.\n *\n * That GET acts is deliberate, and the opposite of the platform's newsletter\n * links. A person who used their mail client's Unsubscribe command has asked\n * already, and a second button would be a way out with a hurdle in it. The\n * one caller that could fetch the link without asking — a link scanner — can\n * only make a cold sequence stop writing to somebody, which is the direction\n * cold email should err in.\n *\n * No sign-in: the signature in the token is the authority, and a token that\n * does not verify changes nothing. It is registered as a recipient link, so\n * it answers whether or not Outreach is released to the organization now —\n * an opt-out outlives a paused rollout. Each act is idempotent: the link\n * used twice changes nothing the first use did not.\n *\n * What it records is what a reply saying \"stop\" records: the organization's\n * do-not-contact list, the site's `sales` topic, and every open enrollment\n * of the address stopped. An enrollment that no longer exists belongs to a\n * person or a workspace already erased, whose every site already refuses\n * them; the page says the same thing either way, and reveals nothing.\n */\n\nconst PAGE_HEADERS = {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Cache-Control': 'no-store',\n 'X-Robots-Tag': 'noindex',\n 'Referrer-Policy': 'no-referrer',\n}\n\nconst escapeHtml = (value: string) =>\n value.replace(/[&<>\"']/g, (character) => `&#${character.charCodeAt(0)};`)\n\n/** The plain page both methods answer with. */\nexport function outreachUnsubscribePage(input: { title: string; body: string }, status = 200): Response {\n const html =\n '<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">' +\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' +\n // The reader's own colors, light or dark: the page has no theme to take.\n '<meta name=\"color-scheme\" content=\"light dark\">' +\n `<title>${escapeHtml(input.title)}</title>` +\n '<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;line-height:1.5}h1{font-size:1.5rem}</style>' +\n `</head><body><main><h1>${escapeHtml(input.title)}</h1><p>${escapeHtml(input.body)}</p></main></body></html>`\n return new Response(html, { status, headers: PAGE_HEADERS })\n}\n\nexport function createOutreachUnsubscribeRoute(\n deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now' | 'optOutOfSalesTopic' | 'stampRecordEmailState' | 'timeline'>,\n): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'GET' && request.method !== 'POST') {\n return new Response('Method not allowed', { status: 405, headers: { Allow: 'GET, POST' } })\n }\n const target = readOutreachUnsubscribeToken(new URL(request.url).searchParams.get('t'))\n if (!target) {\n return outreachUnsubscribePage(\n {\n title: 'This link doesn’t work',\n body: 'This unsubscribe link isn’t valid. To stop these emails, reply to one of them with “no”.',\n },\n 400,\n )\n }\n const firestore = deps.firestore()\n const snapshot = await outreachOrgCollection(firestore, target.orgId, 'enrollments').doc(target.enrollmentId).get()\n const enrollment = readStoredOutreachEnrollment(target.enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (enrollment) {\n await recordOutreachOptOut(deps, {\n orgId: target.orgId,\n email: enrollment.email,\n source: 'unsubscribe',\n enrollment,\n hostIds: [enrollment.hostId],\n detail: request.method === 'POST' ? 'One-click unsubscribe.' : 'Used the unsubscribe link.',\n nowMs: deps.now(),\n })\n }\n const settings = await readOutreachComplianceSettingsDoc(firestore, target.orgId).catch(() => null)\n const sender = settings?.brandName || settings?.legalName || 'The sender'\n return outreachUnsubscribePage({\n title: 'You’re unsubscribed',\n body: `${sender} won’t send you any more of these emails.`,\n })\n }\n}\n"],"names":["readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachEnrollment","recordOutreachOptOut","readOutreachUnsubscribeToken","PAGE_HEADERS","escapeHtml","value","replace","character","charCodeAt","outreachUnsubscribePage","input","status","html","title","body","Response","headers","createOutreachUnsubscribeRoute","deps","request","method","Allow","target","URL","url","searchParams","get","firestore","snapshot","orgId","doc","enrollmentId","enrollment","exists","data","undefined","email","source","hostIds","hostId","detail","nowMs","now","settings","catch","sender","brandName","legalName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,iCAAiC,QAAQ,0CAAsC;AACxF,SAASC,qBAAqB,EAAEC,4BAA4B,QAAQ,iCAA6B;AACjG,SAASC,oBAAoB,QAAQ,yBAAqB;AAE1D,SAASC,4BAA4B,QAAQ,wBAAoB;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GAED,MAAMC,eAAe;IACnB,gBAAgB;IAChB,iBAAiB;IACjB,gBAAgB;IAChB,mBAAmB;AACrB;AAEA,MAAMC,aAAa,CAACC,QAClBA,MAAMC,OAAO,CAAC,YAAY,CAACC,YAAc,CAAC,EAAE,EAAEA,UAAUC,UAAU,CAAC,GAAG,CAAC,CAAC;AAE1E,6CAA6C,GAC7C,OAAO,SAASC,wBAAwBC,KAAsC,EAAEC,SAAS,GAAG;IAC1F,MAAMC,OACJ,gEACA,yEACA,yEAAyE;IACzE,oDACA,CAAC,OAAO,EAAER,WAAWM,MAAMG,KAAK,EAAE,QAAQ,CAAC,GAC3C,8IACA,CAAC,uBAAuB,EAAET,WAAWM,MAAMG,KAAK,EAAE,QAAQ,EAAET,WAAWM,MAAMI,IAAI,EAAE,yBAAyB,CAAC;IAC/G,OAAO,IAAIC,SAASH,MAAM;QAAED;QAAQK,SAASb;IAAa;AAC5D;AAEA,OAAO,SAASc,+BACdC,IAAkH;IAElH,OAAO,OAAOC;QACZ,IAAIA,QAAQC,MAAM,KAAK,SAASD,QAAQC,MAAM,KAAK,QAAQ;YACzD,OAAO,IAAIL,SAAS,sBAAsB;gBAAEJ,QAAQ;gBAAKK,SAAS;oBAAEK,OAAO;gBAAY;YAAE;QAC3F;QACA,MAAMC,SAASpB,6BAA6B,IAAIqB,IAAIJ,QAAQK,GAAG,EAAEC,YAAY,CAACC,GAAG,CAAC;QAClF,IAAI,CAACJ,QAAQ;YACX,OAAOb,wBACL;gBACEI,OAAO;gBACPC,MAAM;YACR,GACA;QAEJ;QACA,MAAMa,YAAYT,KAAKS,SAAS;QAChC,MAAMC,WAAW,MAAM7B,sBAAsB4B,WAAWL,OAAOO,KAAK,EAAE,eAAeC,GAAG,CAACR,OAAOS,YAAY,EAAEL,GAAG;QACjH,MAAMM,aAAahC,6BAA6BsB,OAAOS,YAAY,EAAEH,SAASK,MAAM,GAAGL,SAASM,IAAI,KAAKC;QACzG,IAAIH,YAAY;YACd,MAAM/B,qBAAqBiB,MAAM;gBAC/BW,OAAOP,OAAOO,KAAK;gBACnBO,OAAOJ,WAAWI,KAAK;gBACvBC,QAAQ;gBACRL;gBACAM,SAAS;oBAACN,WAAWO,MAAM;iBAAC;gBAC5BC,QAAQrB,QAAQC,MAAM,KAAK,SAAS,2BAA2B;gBAC/DqB,OAAOvB,KAAKwB,GAAG;YACjB;QACF;QACA,MAAMC,WAAW,MAAM7C,kCAAkC6B,WAAWL,OAAOO,KAAK,EAAEe,KAAK,CAAC,IAAM;QAC9F,MAAMC,SAASF,CAAAA,4BAAAA,SAAUG,SAAS,MAAIH,4BAAAA,SAAUI,SAAS,KAAI;QAC7D,OAAOtC,wBAAwB;YAC7BI,OAAO;YACPC,MAAM,GAAG+B,OAAO,yCAAyC,CAAC;QAC5D;IACF;AACF"}
|