@aglyn/plugins-outreach 1.0.0-beta.275 → 1.0.0-beta.277
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +14 -14
- package/src/lib/components/reply-dialog.d.ts +2 -1
- package/src/lib/components/reply-dialog.js +1 -1
- package/src/lib/components/reply-dialog.js.map +1 -1
- package/src/lib/engine/manual-reply.d.ts +15 -1
- package/src/lib/engine/manual-reply.js +12 -2
- package/src/lib/engine/manual-reply.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +2 -0
- package/src/lib/model/enrollment-history.js +10 -5
- package/src/lib/model/enrollment-history.js.map +1 -1
- package/src/lib/model/enrollment-timeline.js +9 -4
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +2 -0
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +6 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/routes/register-routes.d.ts +2 -1
- package/src/lib/routes/register-routes.js +10 -2
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/reply-routes.d.ts +16 -0
- package/src/lib/routes/reply-routes.js +96 -15
- package/src/lib/routes/reply-routes.js.map +1 -1
- package/src/lib/runtime/click-events.d.ts +13 -0
- package/src/lib/runtime/click-events.js +19 -6
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-link.d.ts +8 -0
- package/src/lib/runtime/click-link.js +7 -0
- package/src/lib/runtime/click-link.js.map +1 -1
|
@@ -20,10 +20,12 @@ import { outreachThreadMessageFromGmail } from "../engine/thread-message.js";
|
|
|
20
20
|
import { mailboxRef } from "../mailboxes/mailbox-credentials.js";
|
|
21
21
|
import { outreachLocalDay } from "../mailboxes/mailbox-settings.js";
|
|
22
22
|
import { markOutreachMailboxReconnectRequired } from "../mailboxes/mailbox-transport.js";
|
|
23
|
+
import { OUTREACH_COLLECTIONS } from "../model/outreach.types.js";
|
|
24
|
+
import { newOutreachLinkId, outreachStoredLink } from "../runtime/click-link.js";
|
|
23
25
|
import { fileOutreachEmail } from "../runtime/timeline.js";
|
|
24
26
|
import { outreachUnsubscribeMailbox } from "../runtime/unsubscribe-link.js";
|
|
25
27
|
import { lookupOutreachDoNotContact, lookupOutreachDoNotContactDomains } from "../storage/do-not-contact-store.js";
|
|
26
|
-
import { outreachEnrollmentLink, outreachOrgCollection, readStoredOutreachEnrollment, readStoredOutreachMailbox } from "../storage/outreach-records.js";
|
|
28
|
+
import { outreachEnrollmentLink, outreachOrgCollection, readStoredOutreachEnrollment, readStoredOutreachMailbox, readStoredOutreachSequence } from "../storage/outreach-records.js";
|
|
27
29
|
import { GmailTransportError, googleFailureAnswer, isReconnectRequired } from "../transport/gmail-errors.js";
|
|
28
30
|
import { Rfc5322MessageError } from "../transport/rfc5322.js";
|
|
29
31
|
import { sendOutreachReply } from "../transport/send-message.js";
|
|
@@ -62,6 +64,15 @@ import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocu
|
|
|
62
64
|
* the enrollment's `lastSentAtMs` moves, which keeps the thread watched for
|
|
63
65
|
* what the person writes next.
|
|
64
66
|
*
|
|
67
|
+
* ## Its links, on a sequence that tracks them
|
|
68
|
+
*
|
|
69
|
+
* The links in the member's words go out as the short links a step's do
|
|
70
|
+
* (AGL-4022), on the mailbox domain's link host: minted with the answer's
|
|
71
|
+
* reply key, stored before the send, and kept on the `manual_reply` record.
|
|
72
|
+
* A click on one is the person's — on their enrollment and their record —
|
|
73
|
+
* and never the sequence's. A link that cannot be stored goes out as
|
|
74
|
+
* written: an answer whose links work beats one held for a counter.
|
|
75
|
+
*
|
|
65
76
|
* ## Once
|
|
66
77
|
*
|
|
67
78
|
* The composer mints a key per answer, which becomes its `Message-ID`. The
|
|
@@ -73,24 +84,24 @@ import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocu
|
|
|
73
84
|
/** The most of an enrollment's threads one read opens, newest first. */ const THREADS_READ_MAX = 5;
|
|
74
85
|
/** The most messages from the person's address one read opens, outside those threads. */ const OWN_THREAD_MESSAGES_MAX = 10;
|
|
75
86
|
/** A reply key as the composer mints one: a UUID, or anything as plain. */ const REPLY_KEY = /^[A-Za-z0-9][A-Za-z0-9-]{7,63}$/;
|
|
76
|
-
/** The enrollment, and the mailbox an answer goes from: its own, else its sequence's. */ async function loadEnrollment(firestore, orgId, enrollmentId) {
|
|
87
|
+
/** The enrollment, its sequence's click setting, and the mailbox an answer goes from: its own, else its sequence's. */ async function loadEnrollment(firestore, orgId, enrollmentId) {
|
|
77
88
|
const snapshot = await outreachOrgCollection(firestore, orgId, 'enrollments').doc(enrollmentId).get();
|
|
78
89
|
const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined);
|
|
79
90
|
if (!enrollment) return null;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
mailboxId = String(sequence.exists ? (_sequence_get = sequence.get('mailboxId')) != null ? _sequence_get : '' : '');
|
|
85
|
-
}
|
|
91
|
+
const stored = enrollment.sequenceId ? await outreachOrgCollection(firestore, orgId, 'sequences').doc(enrollment.sequenceId).get() : null;
|
|
92
|
+
const sequence = stored ? readStoredOutreachSequence(stored.id, stored.exists ? stored.data() : undefined) : null;
|
|
93
|
+
const tracksLinks = (sequence == null ? void 0 : sequence.settings.trackClicks) === true;
|
|
94
|
+
const mailboxId = enrollment.mailboxId || (sequence == null ? void 0 : sequence.mailboxId) || '';
|
|
86
95
|
if (!mailboxId) return {
|
|
87
96
|
enrollment,
|
|
88
|
-
mailbox: null
|
|
97
|
+
mailbox: null,
|
|
98
|
+
tracksLinks
|
|
89
99
|
};
|
|
90
100
|
const box = await mailboxRef(firestore, orgId, mailboxId).get();
|
|
91
101
|
return {
|
|
92
102
|
enrollment,
|
|
93
|
-
mailbox: readStoredOutreachMailbox(mailboxId, box.exists ? box.data() : undefined)
|
|
103
|
+
mailbox: readStoredOutreachMailbox(mailboxId, box.exists ? box.data() : undefined),
|
|
104
|
+
tracksLinks
|
|
94
105
|
};
|
|
95
106
|
}
|
|
96
107
|
/** The member is the mailbox's own, or an organization owner or admin. */ const mayUseMailbox = (caller, mailbox)=>Boolean(mailbox) && ((mailbox == null ? void 0 : mailbox.connectedByUid) === caller.uid || caller.isOrgAdmin);
|
|
@@ -257,12 +268,13 @@ import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachDocu
|
|
|
257
268
|
mayUseMailbox: mayUseMailbox(caller, mailbox)
|
|
258
269
|
});
|
|
259
270
|
}
|
|
260
|
-
/** Where an answer to the newest message would go, from what, under what subject. */ function draftFor(enrollment, mailbox) {
|
|
271
|
+
/** Where an answer to the newest message would go, from what, under what subject. */ function draftFor(enrollment, mailbox, tracksLinks) {
|
|
261
272
|
var _answering_replyTo, _mailbox_provider;
|
|
262
273
|
const answering = outreachMessageToAnswer(enrollment.inboundMessages);
|
|
263
274
|
if (!answering || !mailbox) return null;
|
|
264
275
|
return {
|
|
265
276
|
answerId: answering.id,
|
|
277
|
+
tracksLinks,
|
|
266
278
|
to: (_answering_replyTo = answering.replyTo) != null ? _answering_replyTo : answering.from,
|
|
267
279
|
subject: outreachReplySubject(answering, enrollment.threadSubject),
|
|
268
280
|
// A mailbox connected before Microsoft ones existed names no provider: it is Google's.
|
|
@@ -296,7 +308,7 @@ const REFUSAL_STATUS = {
|
|
|
296
308
|
const read = await withInbound(deps, replyDeps, caller, loaded);
|
|
297
309
|
if (read.ok === false) return read.response;
|
|
298
310
|
const { enrollment, readMailbox } = read;
|
|
299
|
-
const draft = draftFor(enrollment, loaded.mailbox);
|
|
311
|
+
const draft = draftFor(enrollment, loaded.mailbox, loaded.tracksLinks);
|
|
300
312
|
const refusal = draft ? await refusalFor(firestore, caller, enrollment, loaded.mailbox, draft.to) : null;
|
|
301
313
|
return outreachOk({
|
|
302
314
|
ok: true,
|
|
@@ -414,6 +426,16 @@ const REFUSAL_STATUS = {
|
|
|
414
426
|
console.error('[outreach] an answer’s claim could not be handed back', error);
|
|
415
427
|
});
|
|
416
428
|
const nowMs = deps.now();
|
|
429
|
+
const sending = await withTrackedLinks(firestore, replyDeps, {
|
|
430
|
+
orgId: caller.orgId,
|
|
431
|
+
loaded,
|
|
432
|
+
answering,
|
|
433
|
+
body: body['body'],
|
|
434
|
+
replyKey,
|
|
435
|
+
senderAddress: sender.address,
|
|
436
|
+
untracked: reply,
|
|
437
|
+
nowMs
|
|
438
|
+
});
|
|
417
439
|
const opened = await replyDeps.openMailbox(mailbox.id);
|
|
418
440
|
if (opened.ok === false) {
|
|
419
441
|
await release();
|
|
@@ -430,7 +452,7 @@ const REFUSAL_STATUS = {
|
|
|
430
452
|
}
|
|
431
453
|
let sent;
|
|
432
454
|
try {
|
|
433
|
-
sent = await sendOutreachReply(opened.client,
|
|
455
|
+
sent = await sendOutreachReply(opened.client, sending, sender, {
|
|
434
456
|
messageIdLocalPart: replyKey,
|
|
435
457
|
messageIdDomain: domain
|
|
436
458
|
});
|
|
@@ -460,7 +482,7 @@ const REFUSAL_STATUS = {
|
|
|
460
482
|
byUid: caller.uid,
|
|
461
483
|
from: sender.address,
|
|
462
484
|
to: reply.to,
|
|
463
|
-
body:
|
|
485
|
+
body: sending.body,
|
|
464
486
|
replyKey,
|
|
465
487
|
gmailMessageId: sent.gmailMessageId,
|
|
466
488
|
gmailThreadId: sent.threadId,
|
|
@@ -468,6 +490,8 @@ const REFUSAL_STATUS = {
|
|
|
468
490
|
subject: sent.subject
|
|
469
491
|
}, reply.inReplyTo ? {
|
|
470
492
|
inReplyTo: reply.inReplyTo
|
|
493
|
+
} : {}, sending.trackedLinks.length ? {
|
|
494
|
+
links: sending.trackedLinks
|
|
471
495
|
} : {});
|
|
472
496
|
const enrollment = await firestore.runTransaction(async (transaction)=>{
|
|
473
497
|
var _current_stepRecords;
|
|
@@ -512,7 +536,7 @@ const REFUSAL_STATUS = {
|
|
|
512
536
|
to: reply.to,
|
|
513
537
|
messageId: sent.messageId,
|
|
514
538
|
inReplyTo: reply.inReplyTo,
|
|
515
|
-
body:
|
|
539
|
+
body: sending.body,
|
|
516
540
|
atMs,
|
|
517
541
|
byUid: caller.uid,
|
|
518
542
|
byName: mailbox.connectedByUid === caller.uid ? mailbox.displayName || null : null
|
|
@@ -527,6 +551,63 @@ const REFUSAL_STATUS = {
|
|
|
527
551
|
});
|
|
528
552
|
};
|
|
529
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* The answer as it goes out (AGL-4022): on a sequence that tracks link
|
|
556
|
+
* clicks, written again with its links minted as short links on the mailbox
|
|
557
|
+
* domain's link host, each stored before the send and named by the answer's
|
|
558
|
+
* reply key; otherwise — or when the links cannot be stored — exactly as
|
|
559
|
+
* the member wrote it.
|
|
560
|
+
*/ async function withTrackedLinks(firestore, replyDeps, input) {
|
|
561
|
+
var _loaded_mailbox;
|
|
562
|
+
const { loaded, nowMs } = input;
|
|
563
|
+
if (!loaded.tracksLinks) return input.untracked;
|
|
564
|
+
let origin = null;
|
|
565
|
+
if (replyDeps.clickLinkOrigin) {
|
|
566
|
+
origin = await replyDeps.clickLinkOrigin({
|
|
567
|
+
orgId: input.orgId,
|
|
568
|
+
senderAddress: input.senderAddress
|
|
569
|
+
}).catch((error)=>{
|
|
570
|
+
console.warn('[outreach] the click-tracking host could not be read; an answer’s links use the app address', error);
|
|
571
|
+
return null;
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
const minted = [];
|
|
575
|
+
const tracked = composeOutreachManualReply({
|
|
576
|
+
enrollment: loaded.enrollment,
|
|
577
|
+
answering: input.answering,
|
|
578
|
+
body: input.body,
|
|
579
|
+
timeZone: (_loaded_mailbox = loaded.mailbox) == null ? void 0 : _loaded_mailbox.timezone,
|
|
580
|
+
rewriteLink: (link)=>{
|
|
581
|
+
const doc = outreachStoredLink({
|
|
582
|
+
orgId: input.orgId,
|
|
583
|
+
enrollmentId: loaded.enrollment.id,
|
|
584
|
+
stepIndex: loaded.enrollment.stepIndex,
|
|
585
|
+
linkIndex: link.index,
|
|
586
|
+
url: link.url,
|
|
587
|
+
answerKey: input.replyKey
|
|
588
|
+
}, nowMs);
|
|
589
|
+
const id = newOutreachLinkId();
|
|
590
|
+
const url = doc ? replyDeps.clickLinkUrl(id, origin) : null;
|
|
591
|
+
if (!doc || !url) return null;
|
|
592
|
+
minted.push({
|
|
593
|
+
id,
|
|
594
|
+
doc
|
|
595
|
+
});
|
|
596
|
+
return url;
|
|
597
|
+
}
|
|
598
|
+
}).reply;
|
|
599
|
+
if (!tracked || !minted.length) return input.untracked;
|
|
600
|
+
try {
|
|
601
|
+
const batch = firestore.batch();
|
|
602
|
+
for (const link of minted)batch.create(firestore.collection(OUTREACH_COLLECTIONS.links).doc(link.id), link.doc);
|
|
603
|
+
await batch.commit();
|
|
604
|
+
return tracked;
|
|
605
|
+
} catch (error) {
|
|
606
|
+
// A link that resolves nowhere is worse than one as written.
|
|
607
|
+
console.error('[outreach] an answer’s short links could not be stored; it carries its links as written', error);
|
|
608
|
+
return input.untracked;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
530
611
|
/**
|
|
531
612
|
* One more answer on the mailbox's local day, in
|
|
532
613
|
* `health.daily.<day>.manualReplies` beside the day's sends and never in
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/reply-routes.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 { randomUUID } from 'node:crypto'\nimport {\n composeOutreachManualReply,\n mergeOutreachInboundMessages,\n outreachEnrollmentHasInbound,\n outreachInboundMessagesOf,\n outreachMailboxAddress,\n outreachManualReplyRefusal,\n outreachMessageToAnswer,\n outreachReplySubject,\n type OutreachManualReplyRefusal,\n} from '../engine/manual-reply'\nimport { outreachThreadMessageFromGmail, type OutreachThreadMessage } from '../engine/thread-message'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport { outreachLocalDay } from '../mailboxes/mailbox-settings'\nimport {\n markOutreachMailboxReconnectRequired,\n type OpenedOutreachMailbox,\n} from '../mailboxes/mailbox-transport'\nimport type {\n OutreachEnrollmentReplyResponse,\n OutreachEnrollmentThreadResponse,\n OutreachReplyDraft,\n} from '../model/outreach-api'\nimport type {\n OutreachEnrollment,\n OutreachInboundMessage,\n OutreachMailbox,\n OutreachStepRecord,\n} from '../model/outreach.types'\nimport { fileOutreachEmail } from '../runtime/timeline'\nimport { outreachUnsubscribeMailbox } from '../runtime/unsubscribe-link'\nimport {\n lookupOutreachDoNotContact,\n lookupOutreachDoNotContactDomains,\n} from '../storage/do-not-contact-store'\nimport {\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n} from '../storage/outreach-records'\nimport type { OutreachMailClient } from '../transport/mail-client'\nimport { GmailTransportError, googleFailureAnswer, isReconnectRequired } from '../transport/gmail-errors'\nimport { Rfc5322MessageError } from '../transport/rfc5322'\nimport { sendOutreachReply } from '../transport/send-message'\nimport type { OutreachEnrollRouteDeps } from './enroll-routes'\nimport type { OutreachRouteCaller } from './route-gate'\nimport { outreachRouteGate } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ANSWERING A PERSON WHO WROTE BACK (AGL-4020): `outreach/enrollments/thread`\n * and `outreach/enrollments/reply`.\n *\n * A person who answered a sequence email is answered by a member, from the\n * enrollment's own mailbox, in the same thread — never from the platform's\n * mail service, which carries no prospecting, and never as a new thread.\n * The engine writes the answer (`engine/manual-reply.ts`); these routes read\n * what it answers, check what still refuses, and send it.\n *\n * ## What the person wrote\n *\n * The sync keeps every message of theirs on the enrollment. A reply synced\n * before they were kept is on the enrollment only as an id, so when the\n * enrollment says someone wrote back and keeps nothing they wrote, the\n * `thread` route reads the enrollment's threads in the mailbox — and, when\n * they answered in a thread of their own, the messages from their address\n * since they were enrolled — and keeps what it finds. That read is made only\n * for a member who may send from the mailbox: nobody reads another member's\n * mail through these routes.\n *\n * ## The answer is no step of the sequence\n *\n * It is kept as a `manual_reply` step record and filed on the person's CRM\n * record. It never restarts the sequence or moves its step; it never counts\n * against the mailbox's daily cap, joins the sends a bounce rate is judged\n * over, or bumps the sequence's figures. The mailbox counts it apart, on\n * `health.daily.<day>.manualReplies`, so it can never pause the mailbox.\n * Its `Message-ID` joins the enrollment's when it went into the current\n * thread, so a later step of an enrollment still running answers it, and\n * the enrollment's `lastSentAtMs` moves, which keeps the thread watched for\n * what the person writes next.\n *\n * ## Once\n *\n * The composer mints a key per answer, which becomes its `Message-ID`. The\n * answer is claimed in a transaction before it is sent: a retry of the same\n * answer that already went is answered with its record, and a second answer\n * while one is in flight is refused. A send that fails hands the claim back.\n */\n\n/** Answers one mailbox may send in an hour: a conversation, not a campaign. */\nexport const OUTREACH_MANUAL_REPLIES_PER_HOUR = 30\n\n/** How long a claim on an answer holds before another answer may go. */\nconst REPLY_CLAIM_STALE_MS = 2 * 60_000\n\n/** The most of an enrollment's threads one read opens, newest first. */\nconst THREADS_READ_MAX = 5\n\n/** The most messages from the person's address one read opens, outside those threads. */\nconst OWN_THREAD_MESSAGES_MAX = 10\n\n/** A reply key as the composer mints one: a UUID, or anything as plain. */\nconst REPLY_KEY = /^[A-Za-z0-9][A-Za-z0-9-]{7,63}$/\n\n/** The routes' reach beyond the shared route deps: the mailbox, and the platform's rate limit. */\nexport interface OutreachReplyDeps {\n /** Opens a mailbox's client from its sealed grant. */\n openMailbox(mailboxId: string): Promise<OpenedOutreachMailbox>\n consumeRateLimit(key: string, options: { limit: number; windowMs: number }): Promise<{ allowed: boolean }>\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\ninterface Loaded {\n enrollment: OutreachEnrollment\n mailbox: OutreachMailbox | null\n}\n\n/** The enrollment, and the mailbox an answer goes from: its own, else its sequence's. */\nasync function loadEnrollment(firestore: Firestore, orgId: string, enrollmentId: string): Promise<Loaded | null> {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'enrollments').doc(enrollmentId).get()\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return null\n let mailboxId = enrollment.mailboxId\n if (!mailboxId && enrollment.sequenceId) {\n const sequence = await outreachOrgCollection(firestore, orgId, 'sequences').doc(enrollment.sequenceId).get()\n mailboxId = String(sequence.exists ? (sequence.get('mailboxId') ?? '') : '')\n }\n if (!mailboxId) return { enrollment, mailbox: null }\n const box = await mailboxRef(firestore, orgId, mailboxId).get()\n return { enrollment, mailbox: readStoredOutreachMailbox(mailboxId, box.exists ? box.data() : undefined) }\n}\n\n/** The member is the mailbox's own, or an organization owner or admin. */\nconst mayUseMailbox = (caller: OutreachRouteCaller, mailbox: OutreachMailbox | null) =>\n Boolean(mailbox) && (mailbox?.connectedByUid === caller.uid || caller.isOrgAdmin)\n\n/** The mailbox's own addresses: the account's, and every alias it sends as. */\nconst selfAddressesOf = (mailbox: OutreachMailbox) =>\n [mailbox.email, mailbox.sendAs, ...(mailbox.sendAsOptions ?? []).map((option) => option.email)].filter(Boolean)\n\n/**\n * What the person wrote, read from the mailbox: the enrollment's threads,\n * newest first, and — when none holds a message of theirs — the messages\n * from their address since they were enrolled.\n */\nasync function readMailboxInbound(\n client: OutreachMailClient,\n enrollment: OutreachEnrollment,\n mailbox: OutreachMailbox,\n): Promise<OutreachInboundMessage[]> {\n const context = {\n selfAddresses: selfAddressesOf(mailbox),\n unsubscribeAddress: outreachUnsubscribeMailbox(mailbox.email)?.address ?? null,\n threadSubject: enrollment.threadSubject,\n }\n const threads = [\n ...new Set([...(enrollment.gmailThreadIds ?? []), enrollment.gmailThreadId].filter((id): id is string => Boolean(id))),\n ]\n .reverse()\n .slice(0, THREADS_READ_MAX)\n const messages: OutreachThreadMessage[] = []\n for (const threadId of threads) {\n const thread = await client.getFullThread(threadId)\n messages.push(...thread.messages.map(outreachThreadMessageFromGmail))\n }\n let found = outreachInboundMessagesOf(messages, context)\n if (!found.length && enrollment.email) {\n const listed = await client.searchMessages(\n { afterMs: enrollment.createdAtMs || 0, from: [enrollment.email] },\n { maxResults: OWN_THREAD_MESSAGES_MAX },\n )\n const own: OutreachThreadMessage[] = []\n for (const stub of listed.messages.slice(0, OWN_THREAD_MESSAGES_MAX)) {\n own.push(outreachThreadMessageFromGmail(await client.getFullMessage(stub.id)))\n }\n found = outreachInboundMessagesOf(own, context)\n }\n return found\n}\n\n/** Keeps what the mailbox held on the enrollment; answers the enrollment as stored. */\nasync function keepInbound(\n firestore: Firestore,\n orgId: string,\n enrollment: OutreachEnrollment,\n found: readonly OutreachInboundMessage[],\n nowMs: number,\n): Promise<OutreachEnrollment> {\n const ref = outreachOrgCollection(firestore, orgId, 'enrollments').doc(enrollment.id)\n return firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const current = readStoredOutreachEnrollment(enrollment.id, snapshot.exists ? snapshot.data() : undefined)\n if (!current) return enrollment\n const inboundMessages = mergeOutreachInboundMessages(current.inboundMessages, found)\n transaction.update(ref, { inboundMessages, updatedAtMs: nowMs })\n return { ...current, inboundMessages, updatedAtMs: nowMs }\n })\n}\n\ntype InboundRead =\n | { ok: true; enrollment: OutreachEnrollment; readMailbox: boolean }\n | { ok: false; response: Response }\n\n/**\n * The enrollment with what the person wrote: as kept, or — when it says\n * they wrote back and keeps nothing — read from the mailbox and kept.\n */\nasync function withInbound(\n deps: OutreachEnrollRouteDeps,\n replyDeps: OutreachReplyDeps,\n caller: OutreachRouteCaller,\n loaded: Loaded,\n): Promise<InboundRead> {\n const { enrollment, mailbox } = loaded\n const unread =\n !enrollment.inboundMessages?.length &&\n outreachEnrollmentHasInbound(enrollment) &&\n mailbox !== null &&\n (mailbox.status === 'connected' || mailbox.status === 'paused') &&\n mayUseMailbox(caller, mailbox)\n if (!unread || !mailbox) return { ok: true, enrollment, readMailbox: false }\n const firestore = deps.firestore()\n const nowMs = deps.now()\n const opened = await replyDeps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n if (opened.reason === 'not-configured') return { ok: true, enrollment, readMailbox: false }\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: caller.orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n mailbox.status = 'reconnect_required'\n return { ok: true, enrollment, readMailbox: false }\n }\n let found: OutreachInboundMessage[]\n try {\n found = await readMailboxInbound(opened.client, enrollment, mailbox)\n } catch (error) {\n if (isReconnectRequired(error)) {\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: caller.orgId,\n mailboxId: mailbox.id,\n errorCode: error instanceof GmailTransportError ? error.code : 'invalid_grant',\n nowMs,\n })\n mailbox.status = 'reconnect_required'\n return { ok: true, enrollment, readMailbox: false }\n }\n if (!(error instanceof GmailTransportError)) throw error\n const answer = googleFailureAnswer(error, 'reply thread read')\n return { ok: false, response: outreachRefusal(answer.status, answer.reason, answer.error) }\n }\n if (!found.length) return { ok: true, enrollment, readMailbox: true }\n return { ok: true, enrollment: await keepInbound(firestore, caller.orgId, enrollment, found, nowMs), readMailbox: true }\n}\n\n/** Every gate an answer still passes — see `outreachManualReplyRefusal`. */\nasync function refusalFor(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n enrollment: OutreachEnrollment,\n mailbox: OutreachMailbox | null,\n to: string | null,\n): Promise<OutreachManualReplyRefusal | null> {\n const recipients = [...new Set([enrollment.email, to].filter((address): address is string => Boolean(address)))]\n const [addresses, domains] = await Promise.all([\n lookupOutreachDoNotContact(firestore, caller.orgId, recipients),\n lookupOutreachDoNotContactDomains(firestore, caller.orgId, recipients),\n ])\n const fold = (answers: Map<string, boolean | null>): boolean | null => {\n const values = recipients.map((address) => answers.get(address) ?? null)\n if (values.includes(true)) return true\n return values.includes(null) ? null : false\n }\n return outreachManualReplyRefusal({\n enrollment,\n doNotContact: fold(addresses),\n doNotContactDomain: fold(domains),\n mailbox,\n mayUseMailbox: mayUseMailbox(caller, mailbox),\n })\n}\n\n/** Where an answer to the newest message would go, from what, under what subject. */\nfunction draftFor(enrollment: OutreachEnrollment, mailbox: OutreachMailbox | null): OutreachReplyDraft | null {\n const answering = outreachMessageToAnswer(enrollment.inboundMessages)\n if (!answering || !mailbox) return null\n return {\n answerId: answering.id,\n to: answering.replyTo ?? answering.from,\n subject: outreachReplySubject(answering, enrollment.threadSubject),\n // A mailbox connected before Microsoft ones existed names no provider: it is Google's.\n from: { address: outreachMailboxAddress(mailbox), name: mailbox.displayName || '', provider: mailbox.provider ?? 'google' },\n }\n}\n\nconst REFUSAL_STATUS: Record<OutreachManualReplyRefusal['code'], number> = {\n 'opted-out': 409,\n bounced: 409,\n 'do-not-contact': 409,\n unchecked: 503,\n 'mailbox-unavailable': 409,\n permission: 403,\n}\n\n/** `outreach/enrollments/thread` — see the module note. */\nexport function createOutreachEnrollmentThreadRoute(\n deps: OutreachEnrollRouteDeps,\n replyDeps: OutreachReplyDeps,\n): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n if (!enrollmentId) return outreachRefusal(400, 'invalid-request', 'Name the enrollment.')\n const firestore = deps.firestore()\n const loaded = await loadEnrollment(firestore, caller.orgId, enrollmentId)\n if (!loaded) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n const read = await withInbound(deps, replyDeps, caller, loaded)\n if (read.ok === false) return read.response\n const { enrollment, readMailbox } = read\n const draft = draftFor(enrollment, loaded.mailbox)\n const refusal = draft ? await refusalFor(firestore, caller, enrollment, loaded.mailbox, draft.to) : null\n return outreachOk({\n ok: true,\n enrollment,\n inboundMessages: enrollment.inboundMessages ?? [],\n draft,\n refusal,\n readMailbox,\n } satisfies OutreachEnrollmentThreadResponse)\n }\n}\n\n/** `outreach/enrollments/reply` — see the module note. */\nexport function createOutreachEnrollmentReplyRoute(\n deps: OutreachEnrollRouteDeps,\n replyDeps: OutreachReplyDeps,\n): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n if (!enrollmentId) return outreachRefusal(400, 'invalid-request', 'Name the enrollment.')\n const named = typeof body['replyKey'] === 'string' ? body['replyKey'].trim() : ''\n if (named && !REPLY_KEY.test(named)) return outreachRefusal(400, 'invalid-request', 'That reply key is not one the composer mints.')\n const replyKey = named || randomUUID()\n const answerId = typeof body['answerId'] === 'string' ? body['answerId'].trim() : null\n\n const firestore = deps.firestore()\n const loaded = await loadEnrollment(firestore, caller.orgId, enrollmentId)\n if (!loaded) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n const read = await withInbound(deps, replyDeps, caller, loaded)\n if (read.ok === false) return read.response\n const { mailbox } = loaded\n const answering = outreachMessageToAnswer(read.enrollment.inboundMessages, answerId)\n if (!answering) {\n return outreachRefusal(\n 409,\n 'nothing-to-answer',\n answerId\n ? 'That message of theirs is not one this enrollment kept.'\n : 'Nothing they wrote could be read, so there is nothing to answer here yet.',\n )\n }\n const composed = composeOutreachManualReply({\n enrollment: read.enrollment,\n answering,\n body: body['body'],\n timeZone: mailbox?.timezone,\n })\n if (composed.error || !composed.reply) {\n return outreachRefusal(400, 'invalid-request', composed.error?.message ?? 'The answer could not be written.')\n }\n const reply = composed.reply\n const refused = await refusalFor(firestore, caller, read.enrollment, mailbox, reply.to)\n if (refused || !mailbox) {\n const refusal = refused ?? { code: 'mailbox-unavailable' as const, message: 'The mailbox is no longer connected.' }\n return outreachRefusal(\n REFUSAL_STATUS[refusal.code],\n refusal.code === 'permission' ? 'permission' : 'reply-refused',\n refusal.message,\n )\n }\n const limited = await replyDeps.consumeRateLimit(`outreach-manual-reply:${mailbox.id}`, {\n limit: OUTREACH_MANUAL_REPLIES_PER_HOUR,\n windowMs: 60 * 60_000,\n })\n if (!limited.allowed) {\n return outreachRefusal(\n 429,\n 'rate-limited',\n `A mailbox can send ${OUTREACH_MANUAL_REPLIES_PER_HOUR} answers an hour. Try again later.`,\n )\n }\n\n const sender = { address: outreachMailboxAddress(mailbox), name: mailbox.displayName || null }\n const domain = sender.address.slice(sender.address.lastIndexOf('@') + 1)\n const messageId = `<${replyKey}@${domain}>`\n const ref = outreachOrgCollection(firestore, caller.orgId, 'enrollments').doc(enrollmentId)\n\n // Claimed before it is sent: a retry of this answer, or a second one in flight.\n const claim = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const current = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!current) return { kind: 'gone' as const }\n const sent = (current.stepRecords ?? []).find(\n (record) => record.kind === 'manual_reply' && (record.replyKey === replyKey || record.messageId === messageId),\n )\n if (sent) return { kind: 'sent' as const, enrollment: current, record: sent }\n const held = current.replyClaim\n if (held && deps.now() - held.atMs < REPLY_CLAIM_STALE_MS) return { kind: 'busy' as const }\n transaction.update(ref, { replyClaim: { messageId, byUid: caller.uid, atMs: deps.now() } })\n return { kind: 'claimed' as const }\n })\n if (claim.kind === 'gone') return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n if (claim.kind === 'busy') {\n return outreachRefusal(409, 'reply-in-flight', 'An answer to them is already being sent. Wait a moment and look again.')\n }\n if (claim.kind === 'sent') {\n return outreachOk({\n ok: true,\n enrollment: claim.enrollment,\n record: claim.record,\n sentTo: claim.record.to ?? reply.to,\n subject: claim.record.subject ?? reply.subject,\n alreadySent: true,\n } satisfies OutreachEnrollmentReplyResponse)\n }\n const release = () =>\n ref.update({ replyClaim: null }).catch((error: unknown) => {\n console.error('[outreach] an answer’s claim could not be handed back', error)\n })\n\n const nowMs = deps.now()\n const opened = await replyDeps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n await release()\n if (opened.reason === 'not-configured') {\n return outreachRefusal(503, 'mailbox-unavailable', 'Mailboxes are not set up on this deployment.')\n }\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: caller.orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n `${sender.address} needs to be connected again. Reconnect it on the Mailboxes page to answer from it.`,\n )\n }\n let sent: Awaited<ReturnType<typeof sendOutreachReply>>\n try {\n sent = await sendOutreachReply(opened.client, reply, sender, {\n messageIdLocalPart: replyKey,\n messageIdDomain: domain,\n })\n } catch (error) {\n await release()\n if (error instanceof Rfc5322MessageError) {\n return outreachRefusal(400, 'invalid-request', `The answer could not be written: ${error.message}`)\n }\n if (error instanceof GmailTransportError && error.reconnectRequired) {\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: caller.orgId,\n mailboxId: mailbox.id,\n errorCode: error.code,\n nowMs,\n })\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n `The mail provider refused ${sender.address}’s access. Reconnect it on the Mailboxes page to answer from it.`,\n )\n }\n const answer = googleFailureAnswer(error, 'manual reply send')\n return outreachRefusal(answer.status, answer.reason, answer.error)\n }\n\n const atMs = deps.now()\n const record: OutreachStepRecord = {\n stepIndex: read.enrollment.stepIndex,\n stepId: '',\n kind: 'manual_reply',\n atMs,\n byUid: caller.uid,\n from: sender.address,\n to: reply.to,\n body: reply.body,\n replyKey,\n gmailMessageId: sent.gmailMessageId,\n gmailThreadId: sent.threadId,\n messageId: sent.messageId,\n subject: sent.subject,\n ...(reply.inReplyTo ? { inReplyTo: reply.inReplyTo } : {}),\n }\n const enrollment = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const current = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!current) return null\n const patch: Record<string, unknown> = {\n stepRecords: [...(current.stepRecords ?? []), record].slice(-20),\n replyClaim: null,\n // The thread is watched for what they write next, and the\n // enrollments table reads this as the last thing that happened.\n lastSentAtMs: atMs,\n updatedAtMs: atMs,\n }\n if (sent.threadId && !current.gmailThreadIds.includes(sent.threadId)) {\n patch['gmailThreadIds'] = [...current.gmailThreadIds, sent.threadId]\n }\n // Into the current thread: a later step answers this answer.\n if (sent.threadId && sent.threadId === current.gmailThreadId && !current.messageIds.includes(sent.messageId)) {\n patch['messageIds'] = [...current.messageIds, sent.messageId]\n }\n transaction.update(ref, patch)\n return { ...current, ...patch } as OutreachEnrollment\n })\n await countManualReply(firestore, caller.orgId, mailbox, atMs)\n await fileOutreachEmail(deps, {\n orgId: caller.orgId,\n hostId: read.enrollment.hostId,\n link: outreachEnrollmentLink(read.enrollment),\n direction: 'outbound',\n subject: sent.subject,\n from: sender.address,\n to: reply.to,\n messageId: sent.messageId,\n inReplyTo: reply.inReplyTo,\n body: reply.body,\n atMs,\n byUid: caller.uid,\n byName: mailbox.connectedByUid === caller.uid ? mailbox.displayName || null : null,\n })\n return outreachOk({\n ok: true,\n enrollment: enrollment ?? read.enrollment,\n record,\n sentTo: reply.to,\n subject: sent.subject,\n alreadySent: false,\n } satisfies OutreachEnrollmentReplyResponse)\n }\n}\n\n/**\n * One more answer on the mailbox's local day, in\n * `health.daily.<day>.manualReplies` beside the day's sends and never in\n * them. Bookkeeping beside the act: the answer has left, so a failure is\n * logged.\n */\nasync function countManualReply(\n firestore: Firestore,\n orgId: string,\n mailbox: OutreachMailbox,\n nowMs: number,\n): Promise<void> {\n const ref = mailboxRef(firestore, orgId, mailbox.id)\n try {\n await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const stored = readStoredOutreachMailbox(mailbox.id, snapshot.exists ? snapshot.data() : undefined)\n if (!stored) return\n const day = outreachLocalDay(nowMs, stored.timezone)\n const counts = stored.health?.daily?.[day] ?? { sent: 0, bounces: 0, replies: 0 }\n const manualReplies = Math.max(0, Number(counts.manualReplies) || 0) + 1\n transaction.update(ref, { [`health.daily.${day}`]: { ...counts, manualReplies }, updatedAtMs: nowMs })\n })\n } catch (error) {\n console.warn('[outreach] an answer could not be counted on its mailbox', error)\n }\n}\n"],"names":["randomUUID","composeOutreachManualReply","mergeOutreachInboundMessages","outreachEnrollmentHasInbound","outreachInboundMessagesOf","outreachMailboxAddress","outreachManualReplyRefusal","outreachMessageToAnswer","outreachReplySubject","outreachThreadMessageFromGmail","mailboxRef","outreachLocalDay","markOutreachMailboxReconnectRequired","fileOutreachEmail","outreachUnsubscribeMailbox","lookupOutreachDoNotContact","lookupOutreachDoNotContactDomains","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","GmailTransportError","googleFailureAnswer","isReconnectRequired","Rfc5322MessageError","sendOutreachReply","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_MANUAL_REPLIES_PER_HOUR","REPLY_CLAIM_STALE_MS","THREADS_READ_MAX","OWN_THREAD_MESSAGES_MAX","REPLY_KEY","loadEnrollment","firestore","orgId","enrollmentId","snapshot","doc","get","enrollment","exists","data","undefined","mailboxId","sequenceId","sequence","String","mailbox","box","mayUseMailbox","caller","Boolean","connectedByUid","uid","isOrgAdmin","selfAddressesOf","email","sendAs","sendAsOptions","map","option","filter","readMailboxInbound","client","context","selfAddresses","unsubscribeAddress","address","threadSubject","threads","Set","gmailThreadIds","gmailThreadId","id","reverse","slice","messages","threadId","thread","getFullThread","push","found","length","listed","searchMessages","afterMs","createdAtMs","from","maxResults","own","stub","getFullMessage","keepInbound","nowMs","ref","runTransaction","transaction","current","inboundMessages","update","updatedAtMs","withInbound","deps","replyDeps","loaded","unread","status","ok","readMailbox","now","opened","openMailbox","reason","errorCode","error","code","answer","response","refusalFor","to","recipients","addresses","domains","Promise","all","fold","answers","values","includes","doNotContact","doNotContactDomain","draftFor","answering","answerId","replyTo","subject","name","displayName","provider","REFUSAL_STATUS","bounced","unchecked","permission","createOutreachEnrollmentThreadRoute","request","method","body","gate","Response","read","draft","refusal","createOutreachEnrollmentReplyRoute","named","trim","test","replyKey","composed","timeZone","timezone","reply","message","refused","limited","consumeRateLimit","limit","windowMs","allowed","sender","domain","lastIndexOf","messageId","claim","kind","sent","stepRecords","find","record","held","replyClaim","atMs","byUid","sentTo","alreadySent","release","catch","console","messageIdLocalPart","messageIdDomain","reconnectRequired","stepIndex","stepId","gmailMessageId","inReplyTo","patch","lastSentAtMs","messageIds","countManualReply","hostId","link","direction","byName","stored","day","counts","health","daily","bounces","replies","manualReplies","Math","max","Number","warn"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/reply-routes.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 { randomUUID } from 'node:crypto'\nimport {\n composeOutreachManualReply,\n type OutreachManualReply,\n mergeOutreachInboundMessages,\n outreachEnrollmentHasInbound,\n outreachInboundMessagesOf,\n outreachMailboxAddress,\n outreachManualReplyRefusal,\n outreachMessageToAnswer,\n outreachReplySubject,\n type OutreachManualReplyRefusal,\n} from '../engine/manual-reply'\nimport { outreachThreadMessageFromGmail, type OutreachThreadMessage } from '../engine/thread-message'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport { outreachLocalDay } from '../mailboxes/mailbox-settings'\nimport {\n markOutreachMailboxReconnectRequired,\n type OpenedOutreachMailbox,\n} from '../mailboxes/mailbox-transport'\nimport type {\n OutreachEnrollmentReplyResponse,\n OutreachEnrollmentThreadResponse,\n OutreachReplyDraft,\n} from '../model/outreach-api'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachEnrollment,\n type OutreachInboundMessage,\n type OutreachMailbox,\n type OutreachStepRecord,\n} from '../model/outreach.types'\nimport { newOutreachLinkId, outreachStoredLink, type OutreachStoredLink } from '../runtime/click-link'\nimport { fileOutreachEmail } from '../runtime/timeline'\nimport { outreachUnsubscribeMailbox } from '../runtime/unsubscribe-link'\nimport {\n lookupOutreachDoNotContact,\n lookupOutreachDoNotContactDomains,\n} from '../storage/do-not-contact-store'\nimport {\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport type { OutreachMailClient } from '../transport/mail-client'\nimport { GmailTransportError, googleFailureAnswer, isReconnectRequired } from '../transport/gmail-errors'\nimport { Rfc5322MessageError } from '../transport/rfc5322'\nimport { sendOutreachReply } from '../transport/send-message'\nimport type { OutreachEnrollRouteDeps } from './enroll-routes'\nimport type { OutreachRouteCaller } from './route-gate'\nimport { outreachRouteGate } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ANSWERING A PERSON WHO WROTE BACK (AGL-4020): `outreach/enrollments/thread`\n * and `outreach/enrollments/reply`.\n *\n * A person who answered a sequence email is answered by a member, from the\n * enrollment's own mailbox, in the same thread — never from the platform's\n * mail service, which carries no prospecting, and never as a new thread.\n * The engine writes the answer (`engine/manual-reply.ts`); these routes read\n * what it answers, check what still refuses, and send it.\n *\n * ## What the person wrote\n *\n * The sync keeps every message of theirs on the enrollment. A reply synced\n * before they were kept is on the enrollment only as an id, so when the\n * enrollment says someone wrote back and keeps nothing they wrote, the\n * `thread` route reads the enrollment's threads in the mailbox — and, when\n * they answered in a thread of their own, the messages from their address\n * since they were enrolled — and keeps what it finds. That read is made only\n * for a member who may send from the mailbox: nobody reads another member's\n * mail through these routes.\n *\n * ## The answer is no step of the sequence\n *\n * It is kept as a `manual_reply` step record and filed on the person's CRM\n * record. It never restarts the sequence or moves its step; it never counts\n * against the mailbox's daily cap, joins the sends a bounce rate is judged\n * over, or bumps the sequence's figures. The mailbox counts it apart, on\n * `health.daily.<day>.manualReplies`, so it can never pause the mailbox.\n * Its `Message-ID` joins the enrollment's when it went into the current\n * thread, so a later step of an enrollment still running answers it, and\n * the enrollment's `lastSentAtMs` moves, which keeps the thread watched for\n * what the person writes next.\n *\n * ## Its links, on a sequence that tracks them\n *\n * The links in the member's words go out as the short links a step's do\n * (AGL-4022), on the mailbox domain's link host: minted with the answer's\n * reply key, stored before the send, and kept on the `manual_reply` record.\n * A click on one is the person's — on their enrollment and their record —\n * and never the sequence's. A link that cannot be stored goes out as\n * written: an answer whose links work beats one held for a counter.\n *\n * ## Once\n *\n * The composer mints a key per answer, which becomes its `Message-ID`. The\n * answer is claimed in a transaction before it is sent: a retry of the same\n * answer that already went is answered with its record, and a second answer\n * while one is in flight is refused. A send that fails hands the claim back.\n */\n\n/** Answers one mailbox may send in an hour: a conversation, not a campaign. */\nexport const OUTREACH_MANUAL_REPLIES_PER_HOUR = 30\n\n/** How long a claim on an answer holds before another answer may go. */\nconst REPLY_CLAIM_STALE_MS = 2 * 60_000\n\n/** The most of an enrollment's threads one read opens, newest first. */\nconst THREADS_READ_MAX = 5\n\n/** The most messages from the person's address one read opens, outside those threads. */\nconst OWN_THREAD_MESSAGES_MAX = 10\n\n/** A reply key as the composer mints one: a UUID, or anything as plain. */\nconst REPLY_KEY = /^[A-Za-z0-9][A-Za-z0-9-]{7,63}$/\n\n/** The routes' reach beyond the shared route deps: the mailbox, and the platform's rate limit. */\nexport interface OutreachReplyDeps {\n /** Opens a mailbox's client from its sealed grant. */\n openMailbox(mailboxId: string): Promise<OpenedOutreachMailbox>\n consumeRateLimit(key: string, options: { limit: number; windowMs: number }): Promise<{ allowed: boolean }>\n /** The short tracking link for a stored link's id, or `null` when none can be made (AGL-4022). */\n clickLinkUrl(linkId: string, trackingOrigin?: string | null): string | null\n /** The click-tracking origin for mail sent as `senderAddress`, or `null` for the console's. */\n clickLinkOrigin?(input: { orgId: string; senderAddress: string }): Promise<string | null>\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\ninterface Loaded {\n enrollment: OutreachEnrollment\n mailbox: OutreachMailbox | null\n /** The sequence tracks link clicks, so an answer's links go out as short links (AGL-4022). */\n tracksLinks: boolean\n}\n\n/** The enrollment, its sequence's click setting, and the mailbox an answer goes from: its own, else its sequence's. */\nasync function loadEnrollment(firestore: Firestore, orgId: string, enrollmentId: string): Promise<Loaded | null> {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'enrollments').doc(enrollmentId).get()\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return null\n const stored = enrollment.sequenceId\n ? await outreachOrgCollection(firestore, orgId, 'sequences').doc(enrollment.sequenceId).get()\n : null\n const sequence = stored ? readStoredOutreachSequence(stored.id, stored.exists ? stored.data() : undefined) : null\n const tracksLinks = sequence?.settings.trackClicks === true\n const mailboxId = enrollment.mailboxId || sequence?.mailboxId || ''\n if (!mailboxId) return { enrollment, mailbox: null, tracksLinks }\n const box = await mailboxRef(firestore, orgId, mailboxId).get()\n return {\n enrollment,\n mailbox: readStoredOutreachMailbox(mailboxId, box.exists ? box.data() : undefined),\n tracksLinks,\n }\n}\n\n/** The member is the mailbox's own, or an organization owner or admin. */\nconst mayUseMailbox = (caller: OutreachRouteCaller, mailbox: OutreachMailbox | null) =>\n Boolean(mailbox) && (mailbox?.connectedByUid === caller.uid || caller.isOrgAdmin)\n\n/** The mailbox's own addresses: the account's, and every alias it sends as. */\nconst selfAddressesOf = (mailbox: OutreachMailbox) =>\n [mailbox.email, mailbox.sendAs, ...(mailbox.sendAsOptions ?? []).map((option) => option.email)].filter(Boolean)\n\n/**\n * What the person wrote, read from the mailbox: the enrollment's threads,\n * newest first, and — when none holds a message of theirs — the messages\n * from their address since they were enrolled.\n */\nasync function readMailboxInbound(\n client: OutreachMailClient,\n enrollment: OutreachEnrollment,\n mailbox: OutreachMailbox,\n): Promise<OutreachInboundMessage[]> {\n const context = {\n selfAddresses: selfAddressesOf(mailbox),\n unsubscribeAddress: outreachUnsubscribeMailbox(mailbox.email)?.address ?? null,\n threadSubject: enrollment.threadSubject,\n }\n const threads = [\n ...new Set([...(enrollment.gmailThreadIds ?? []), enrollment.gmailThreadId].filter((id): id is string => Boolean(id))),\n ]\n .reverse()\n .slice(0, THREADS_READ_MAX)\n const messages: OutreachThreadMessage[] = []\n for (const threadId of threads) {\n const thread = await client.getFullThread(threadId)\n messages.push(...thread.messages.map(outreachThreadMessageFromGmail))\n }\n let found = outreachInboundMessagesOf(messages, context)\n if (!found.length && enrollment.email) {\n const listed = await client.searchMessages(\n { afterMs: enrollment.createdAtMs || 0, from: [enrollment.email] },\n { maxResults: OWN_THREAD_MESSAGES_MAX },\n )\n const own: OutreachThreadMessage[] = []\n for (const stub of listed.messages.slice(0, OWN_THREAD_MESSAGES_MAX)) {\n own.push(outreachThreadMessageFromGmail(await client.getFullMessage(stub.id)))\n }\n found = outreachInboundMessagesOf(own, context)\n }\n return found\n}\n\n/** Keeps what the mailbox held on the enrollment; answers the enrollment as stored. */\nasync function keepInbound(\n firestore: Firestore,\n orgId: string,\n enrollment: OutreachEnrollment,\n found: readonly OutreachInboundMessage[],\n nowMs: number,\n): Promise<OutreachEnrollment> {\n const ref = outreachOrgCollection(firestore, orgId, 'enrollments').doc(enrollment.id)\n return firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const current = readStoredOutreachEnrollment(enrollment.id, snapshot.exists ? snapshot.data() : undefined)\n if (!current) return enrollment\n const inboundMessages = mergeOutreachInboundMessages(current.inboundMessages, found)\n transaction.update(ref, { inboundMessages, updatedAtMs: nowMs })\n return { ...current, inboundMessages, updatedAtMs: nowMs }\n })\n}\n\ntype InboundRead =\n | { ok: true; enrollment: OutreachEnrollment; readMailbox: boolean }\n | { ok: false; response: Response }\n\n/**\n * The enrollment with what the person wrote: as kept, or — when it says\n * they wrote back and keeps nothing — read from the mailbox and kept.\n */\nasync function withInbound(\n deps: OutreachEnrollRouteDeps,\n replyDeps: OutreachReplyDeps,\n caller: OutreachRouteCaller,\n loaded: Loaded,\n): Promise<InboundRead> {\n const { enrollment, mailbox } = loaded\n const unread =\n !enrollment.inboundMessages?.length &&\n outreachEnrollmentHasInbound(enrollment) &&\n mailbox !== null &&\n (mailbox.status === 'connected' || mailbox.status === 'paused') &&\n mayUseMailbox(caller, mailbox)\n if (!unread || !mailbox) return { ok: true, enrollment, readMailbox: false }\n const firestore = deps.firestore()\n const nowMs = deps.now()\n const opened = await replyDeps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n if (opened.reason === 'not-configured') return { ok: true, enrollment, readMailbox: false }\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: caller.orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n mailbox.status = 'reconnect_required'\n return { ok: true, enrollment, readMailbox: false }\n }\n let found: OutreachInboundMessage[]\n try {\n found = await readMailboxInbound(opened.client, enrollment, mailbox)\n } catch (error) {\n if (isReconnectRequired(error)) {\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: caller.orgId,\n mailboxId: mailbox.id,\n errorCode: error instanceof GmailTransportError ? error.code : 'invalid_grant',\n nowMs,\n })\n mailbox.status = 'reconnect_required'\n return { ok: true, enrollment, readMailbox: false }\n }\n if (!(error instanceof GmailTransportError)) throw error\n const answer = googleFailureAnswer(error, 'reply thread read')\n return { ok: false, response: outreachRefusal(answer.status, answer.reason, answer.error) }\n }\n if (!found.length) return { ok: true, enrollment, readMailbox: true }\n return { ok: true, enrollment: await keepInbound(firestore, caller.orgId, enrollment, found, nowMs), readMailbox: true }\n}\n\n/** Every gate an answer still passes — see `outreachManualReplyRefusal`. */\nasync function refusalFor(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n enrollment: OutreachEnrollment,\n mailbox: OutreachMailbox | null,\n to: string | null,\n): Promise<OutreachManualReplyRefusal | null> {\n const recipients = [...new Set([enrollment.email, to].filter((address): address is string => Boolean(address)))]\n const [addresses, domains] = await Promise.all([\n lookupOutreachDoNotContact(firestore, caller.orgId, recipients),\n lookupOutreachDoNotContactDomains(firestore, caller.orgId, recipients),\n ])\n const fold = (answers: Map<string, boolean | null>): boolean | null => {\n const values = recipients.map((address) => answers.get(address) ?? null)\n if (values.includes(true)) return true\n return values.includes(null) ? null : false\n }\n return outreachManualReplyRefusal({\n enrollment,\n doNotContact: fold(addresses),\n doNotContactDomain: fold(domains),\n mailbox,\n mayUseMailbox: mayUseMailbox(caller, mailbox),\n })\n}\n\n/** Where an answer to the newest message would go, from what, under what subject. */\nfunction draftFor(\n enrollment: OutreachEnrollment,\n mailbox: OutreachMailbox | null,\n tracksLinks: boolean,\n): OutreachReplyDraft | null {\n const answering = outreachMessageToAnswer(enrollment.inboundMessages)\n if (!answering || !mailbox) return null\n return {\n answerId: answering.id,\n tracksLinks,\n to: answering.replyTo ?? answering.from,\n subject: outreachReplySubject(answering, enrollment.threadSubject),\n // A mailbox connected before Microsoft ones existed names no provider: it is Google's.\n from: { address: outreachMailboxAddress(mailbox), name: mailbox.displayName || '', provider: mailbox.provider ?? 'google' },\n }\n}\n\nconst REFUSAL_STATUS: Record<OutreachManualReplyRefusal['code'], number> = {\n 'opted-out': 409,\n bounced: 409,\n 'do-not-contact': 409,\n unchecked: 503,\n 'mailbox-unavailable': 409,\n permission: 403,\n}\n\n/** `outreach/enrollments/thread` — see the module note. */\nexport function createOutreachEnrollmentThreadRoute(\n deps: OutreachEnrollRouteDeps,\n replyDeps: OutreachReplyDeps,\n): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n if (!enrollmentId) return outreachRefusal(400, 'invalid-request', 'Name the enrollment.')\n const firestore = deps.firestore()\n const loaded = await loadEnrollment(firestore, caller.orgId, enrollmentId)\n if (!loaded) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n const read = await withInbound(deps, replyDeps, caller, loaded)\n if (read.ok === false) return read.response\n const { enrollment, readMailbox } = read\n const draft = draftFor(enrollment, loaded.mailbox, loaded.tracksLinks)\n const refusal = draft ? await refusalFor(firestore, caller, enrollment, loaded.mailbox, draft.to) : null\n return outreachOk({\n ok: true,\n enrollment,\n inboundMessages: enrollment.inboundMessages ?? [],\n draft,\n refusal,\n readMailbox,\n } satisfies OutreachEnrollmentThreadResponse)\n }\n}\n\n/** `outreach/enrollments/reply` — see the module note. */\nexport function createOutreachEnrollmentReplyRoute(\n deps: OutreachEnrollRouteDeps,\n replyDeps: OutreachReplyDeps,\n): PluginWebApiHandler {\n return async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n if (!enrollmentId) return outreachRefusal(400, 'invalid-request', 'Name the enrollment.')\n const named = typeof body['replyKey'] === 'string' ? body['replyKey'].trim() : ''\n if (named && !REPLY_KEY.test(named)) return outreachRefusal(400, 'invalid-request', 'That reply key is not one the composer mints.')\n const replyKey = named || randomUUID()\n const answerId = typeof body['answerId'] === 'string' ? body['answerId'].trim() : null\n\n const firestore = deps.firestore()\n const loaded = await loadEnrollment(firestore, caller.orgId, enrollmentId)\n if (!loaded) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n const read = await withInbound(deps, replyDeps, caller, loaded)\n if (read.ok === false) return read.response\n const { mailbox } = loaded\n const answering = outreachMessageToAnswer(read.enrollment.inboundMessages, answerId)\n if (!answering) {\n return outreachRefusal(\n 409,\n 'nothing-to-answer',\n answerId\n ? 'That message of theirs is not one this enrollment kept.'\n : 'Nothing they wrote could be read, so there is nothing to answer here yet.',\n )\n }\n const composed = composeOutreachManualReply({\n enrollment: read.enrollment,\n answering,\n body: body['body'],\n timeZone: mailbox?.timezone,\n })\n if (composed.error || !composed.reply) {\n return outreachRefusal(400, 'invalid-request', composed.error?.message ?? 'The answer could not be written.')\n }\n const reply = composed.reply\n const refused = await refusalFor(firestore, caller, read.enrollment, mailbox, reply.to)\n if (refused || !mailbox) {\n const refusal = refused ?? { code: 'mailbox-unavailable' as const, message: 'The mailbox is no longer connected.' }\n return outreachRefusal(\n REFUSAL_STATUS[refusal.code],\n refusal.code === 'permission' ? 'permission' : 'reply-refused',\n refusal.message,\n )\n }\n const limited = await replyDeps.consumeRateLimit(`outreach-manual-reply:${mailbox.id}`, {\n limit: OUTREACH_MANUAL_REPLIES_PER_HOUR,\n windowMs: 60 * 60_000,\n })\n if (!limited.allowed) {\n return outreachRefusal(\n 429,\n 'rate-limited',\n `A mailbox can send ${OUTREACH_MANUAL_REPLIES_PER_HOUR} answers an hour. Try again later.`,\n )\n }\n\n const sender = { address: outreachMailboxAddress(mailbox), name: mailbox.displayName || null }\n const domain = sender.address.slice(sender.address.lastIndexOf('@') + 1)\n const messageId = `<${replyKey}@${domain}>`\n const ref = outreachOrgCollection(firestore, caller.orgId, 'enrollments').doc(enrollmentId)\n\n // Claimed before it is sent: a retry of this answer, or a second one in flight.\n const claim = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const current = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!current) return { kind: 'gone' as const }\n const sent = (current.stepRecords ?? []).find(\n (record) => record.kind === 'manual_reply' && (record.replyKey === replyKey || record.messageId === messageId),\n )\n if (sent) return { kind: 'sent' as const, enrollment: current, record: sent }\n const held = current.replyClaim\n if (held && deps.now() - held.atMs < REPLY_CLAIM_STALE_MS) return { kind: 'busy' as const }\n transaction.update(ref, { replyClaim: { messageId, byUid: caller.uid, atMs: deps.now() } })\n return { kind: 'claimed' as const }\n })\n if (claim.kind === 'gone') return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n if (claim.kind === 'busy') {\n return outreachRefusal(409, 'reply-in-flight', 'An answer to them is already being sent. Wait a moment and look again.')\n }\n if (claim.kind === 'sent') {\n return outreachOk({\n ok: true,\n enrollment: claim.enrollment,\n record: claim.record,\n sentTo: claim.record.to ?? reply.to,\n subject: claim.record.subject ?? reply.subject,\n alreadySent: true,\n } satisfies OutreachEnrollmentReplyResponse)\n }\n const release = () =>\n ref.update({ replyClaim: null }).catch((error: unknown) => {\n console.error('[outreach] an answer’s claim could not be handed back', error)\n })\n\n const nowMs = deps.now()\n const sending = await withTrackedLinks(firestore, replyDeps, {\n orgId: caller.orgId,\n loaded,\n answering,\n body: body['body'],\n replyKey,\n senderAddress: sender.address,\n untracked: reply,\n nowMs,\n })\n const opened = await replyDeps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n await release()\n if (opened.reason === 'not-configured') {\n return outreachRefusal(503, 'mailbox-unavailable', 'Mailboxes are not set up on this deployment.')\n }\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: caller.orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n `${sender.address} needs to be connected again. Reconnect it on the Mailboxes page to answer from it.`,\n )\n }\n let sent: Awaited<ReturnType<typeof sendOutreachReply>>\n try {\n sent = await sendOutreachReply(opened.client, sending, sender, {\n messageIdLocalPart: replyKey,\n messageIdDomain: domain,\n })\n } catch (error) {\n await release()\n if (error instanceof Rfc5322MessageError) {\n return outreachRefusal(400, 'invalid-request', `The answer could not be written: ${error.message}`)\n }\n if (error instanceof GmailTransportError && error.reconnectRequired) {\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: caller.orgId,\n mailboxId: mailbox.id,\n errorCode: error.code,\n nowMs,\n })\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n `The mail provider refused ${sender.address}’s access. Reconnect it on the Mailboxes page to answer from it.`,\n )\n }\n const answer = googleFailureAnswer(error, 'manual reply send')\n return outreachRefusal(answer.status, answer.reason, answer.error)\n }\n\n const atMs = deps.now()\n const record: OutreachStepRecord = {\n stepIndex: read.enrollment.stepIndex,\n stepId: '',\n kind: 'manual_reply',\n atMs,\n byUid: caller.uid,\n from: sender.address,\n to: reply.to,\n body: sending.body,\n replyKey,\n gmailMessageId: sent.gmailMessageId,\n gmailThreadId: sent.threadId,\n messageId: sent.messageId,\n subject: sent.subject,\n ...(reply.inReplyTo ? { inReplyTo: reply.inReplyTo } : {}),\n ...(sending.trackedLinks.length ? { links: sending.trackedLinks } : {}),\n }\n const enrollment = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const current = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!current) return null\n const patch: Record<string, unknown> = {\n stepRecords: [...(current.stepRecords ?? []), record].slice(-20),\n replyClaim: null,\n // The thread is watched for what they write next, and the\n // enrollments table reads this as the last thing that happened.\n lastSentAtMs: atMs,\n updatedAtMs: atMs,\n }\n if (sent.threadId && !current.gmailThreadIds.includes(sent.threadId)) {\n patch['gmailThreadIds'] = [...current.gmailThreadIds, sent.threadId]\n }\n // Into the current thread: a later step answers this answer.\n if (sent.threadId && sent.threadId === current.gmailThreadId && !current.messageIds.includes(sent.messageId)) {\n patch['messageIds'] = [...current.messageIds, sent.messageId]\n }\n transaction.update(ref, patch)\n return { ...current, ...patch } as OutreachEnrollment\n })\n await countManualReply(firestore, caller.orgId, mailbox, atMs)\n await fileOutreachEmail(deps, {\n orgId: caller.orgId,\n hostId: read.enrollment.hostId,\n link: outreachEnrollmentLink(read.enrollment),\n direction: 'outbound',\n subject: sent.subject,\n from: sender.address,\n to: reply.to,\n messageId: sent.messageId,\n inReplyTo: reply.inReplyTo,\n body: sending.body,\n atMs,\n byUid: caller.uid,\n byName: mailbox.connectedByUid === caller.uid ? mailbox.displayName || null : null,\n })\n return outreachOk({\n ok: true,\n enrollment: enrollment ?? read.enrollment,\n record,\n sentTo: reply.to,\n subject: sent.subject,\n alreadySent: false,\n } satisfies OutreachEnrollmentReplyResponse)\n }\n}\n\n/**\n * The answer as it goes out (AGL-4022): on a sequence that tracks link\n * clicks, written again with its links minted as short links on the mailbox\n * domain's link host, each stored before the send and named by the answer's\n * reply key; otherwise — or when the links cannot be stored — exactly as\n * the member wrote it.\n */\nasync function withTrackedLinks(\n firestore: Firestore,\n replyDeps: OutreachReplyDeps,\n input: {\n orgId: string\n loaded: Loaded\n answering: OutreachInboundMessage\n body: unknown\n replyKey: string\n senderAddress: string\n untracked: OutreachManualReply\n nowMs: number\n },\n): Promise<OutreachManualReply> {\n const { loaded, nowMs } = input\n if (!loaded.tracksLinks) return input.untracked\n let origin: string | null = null\n if (replyDeps.clickLinkOrigin) {\n origin = await replyDeps\n .clickLinkOrigin({ orgId: input.orgId, senderAddress: input.senderAddress })\n .catch((error: unknown) => {\n console.warn('[outreach] the click-tracking host could not be read; an answer’s links use the app address', error)\n return null\n })\n }\n const minted: Array<{ id: string; doc: OutreachStoredLink }> = []\n const tracked = composeOutreachManualReply({\n enrollment: loaded.enrollment,\n answering: input.answering,\n body: input.body,\n timeZone: loaded.mailbox?.timezone,\n rewriteLink: (link) => {\n const doc = outreachStoredLink(\n {\n orgId: input.orgId,\n enrollmentId: loaded.enrollment.id,\n stepIndex: loaded.enrollment.stepIndex,\n linkIndex: link.index,\n url: link.url,\n answerKey: input.replyKey,\n },\n nowMs,\n )\n const id = newOutreachLinkId()\n const url = doc ? replyDeps.clickLinkUrl(id, origin) : null\n if (!doc || !url) return null\n minted.push({ id, doc })\n return url\n },\n }).reply\n if (!tracked || !minted.length) return input.untracked\n try {\n const batch = firestore.batch()\n for (const link of minted) batch.create(firestore.collection(OUTREACH_COLLECTIONS.links).doc(link.id), link.doc)\n await batch.commit()\n return tracked\n } catch (error) {\n // A link that resolves nowhere is worse than one as written.\n console.error('[outreach] an answer’s short links could not be stored; it carries its links as written', error)\n return input.untracked\n }\n}\n\n/**\n * One more answer on the mailbox's local day, in\n * `health.daily.<day>.manualReplies` beside the day's sends and never in\n * them. Bookkeeping beside the act: the answer has left, so a failure is\n * logged.\n */\nasync function countManualReply(\n firestore: Firestore,\n orgId: string,\n mailbox: OutreachMailbox,\n nowMs: number,\n): Promise<void> {\n const ref = mailboxRef(firestore, orgId, mailbox.id)\n try {\n await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const stored = readStoredOutreachMailbox(mailbox.id, snapshot.exists ? snapshot.data() : undefined)\n if (!stored) return\n const day = outreachLocalDay(nowMs, stored.timezone)\n const counts = stored.health?.daily?.[day] ?? { sent: 0, bounces: 0, replies: 0 }\n const manualReplies = Math.max(0, Number(counts.manualReplies) || 0) + 1\n transaction.update(ref, { [`health.daily.${day}`]: { ...counts, manualReplies }, updatedAtMs: nowMs })\n })\n } catch (error) {\n console.warn('[outreach] an answer could not be counted on its mailbox', error)\n }\n}\n"],"names":["randomUUID","composeOutreachManualReply","mergeOutreachInboundMessages","outreachEnrollmentHasInbound","outreachInboundMessagesOf","outreachMailboxAddress","outreachManualReplyRefusal","outreachMessageToAnswer","outreachReplySubject","outreachThreadMessageFromGmail","mailboxRef","outreachLocalDay","markOutreachMailboxReconnectRequired","OUTREACH_COLLECTIONS","newOutreachLinkId","outreachStoredLink","fileOutreachEmail","outreachUnsubscribeMailbox","lookupOutreachDoNotContact","lookupOutreachDoNotContactDomains","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","googleFailureAnswer","isReconnectRequired","Rfc5322MessageError","sendOutreachReply","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_MANUAL_REPLIES_PER_HOUR","REPLY_CLAIM_STALE_MS","THREADS_READ_MAX","OWN_THREAD_MESSAGES_MAX","REPLY_KEY","loadEnrollment","firestore","orgId","enrollmentId","snapshot","doc","get","enrollment","exists","data","undefined","stored","sequenceId","sequence","id","tracksLinks","settings","trackClicks","mailboxId","mailbox","box","mayUseMailbox","caller","Boolean","connectedByUid","uid","isOrgAdmin","selfAddressesOf","email","sendAs","sendAsOptions","map","option","filter","readMailboxInbound","client","context","selfAddresses","unsubscribeAddress","address","threadSubject","threads","Set","gmailThreadIds","gmailThreadId","reverse","slice","messages","threadId","thread","getFullThread","push","found","length","listed","searchMessages","afterMs","createdAtMs","from","maxResults","own","stub","getFullMessage","keepInbound","nowMs","ref","runTransaction","transaction","current","inboundMessages","update","updatedAtMs","withInbound","deps","replyDeps","loaded","unread","status","ok","readMailbox","now","opened","openMailbox","reason","errorCode","error","code","answer","response","refusalFor","to","recipients","addresses","domains","Promise","all","fold","answers","values","includes","doNotContact","doNotContactDomain","draftFor","answering","answerId","replyTo","subject","name","displayName","provider","REFUSAL_STATUS","bounced","unchecked","permission","createOutreachEnrollmentThreadRoute","request","method","body","gate","Response","read","draft","refusal","createOutreachEnrollmentReplyRoute","named","trim","test","replyKey","composed","timeZone","timezone","reply","message","refused","limited","consumeRateLimit","limit","windowMs","allowed","sender","domain","lastIndexOf","messageId","claim","kind","sent","stepRecords","find","record","held","replyClaim","atMs","byUid","sentTo","alreadySent","release","catch","console","sending","withTrackedLinks","senderAddress","untracked","messageIdLocalPart","messageIdDomain","reconnectRequired","stepIndex","stepId","gmailMessageId","inReplyTo","trackedLinks","links","patch","lastSentAtMs","messageIds","countManualReply","hostId","link","direction","byName","input","origin","clickLinkOrigin","warn","minted","tracked","rewriteLink","linkIndex","index","url","answerKey","clickLinkUrl","batch","create","collection","commit","day","counts","health","daily","bounces","replies","manualReplies","Math","max","Number"],"mappings":"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"}
|
|
@@ -43,6 +43,14 @@ import type { OutreachRuntimeDeps } from './runtime-deps';
|
|
|
43
43
|
* 4. **The link rollup** — "which of our links did they follow?", one
|
|
44
44
|
* document holding a bounded map, keyed and capped by the campaign
|
|
45
45
|
* rollup's own rules so a link means the same thing in both reports.
|
|
46
|
+
*
|
|
47
|
+
* A link in a member's ANSWER to a person who wrote back (AGL-4022) is the
|
|
48
|
+
* person's click and not the sequence's: it is written to the enrollment and
|
|
49
|
+
* its history — marked as the answer's — and to their record, but never to
|
|
50
|
+
* the sequence's counters or its link rollup, which report only what the
|
|
51
|
+
* sequence's own steps sent. The route still stamps the person's campaign
|
|
52
|
+
* touch: what they book or fill in next came from the sequence's
|
|
53
|
+
* conversation either way.
|
|
46
54
|
*/
|
|
47
55
|
/** What one recorded click turned out to be. */
|
|
48
56
|
export interface OutreachClickOutcome extends OutreachClickJudgement {
|
|
@@ -53,6 +61,11 @@ export interface OutreachClickOutcome extends OutreachClickJudgement {
|
|
|
53
61
|
}
|
|
54
62
|
/** When the step that carried a link or an image was sent, or `null` when it cannot be found. */
|
|
55
63
|
export declare function outreachStepSentAtMs(enrollment: OutreachEnrollment, stepIndex: number): number | null;
|
|
64
|
+
/**
|
|
65
|
+
* When the email that carried a link was sent: the member's answer it was in
|
|
66
|
+
* (AGL-4022), else the step's email; `null` when it cannot be found.
|
|
67
|
+
*/
|
|
68
|
+
export declare function outreachLinkSentAtMs(enrollment: OutreachEnrollment, target: Pick<OutreachClickTarget, 'stepIndex' | 'answerKey'>): number | null;
|
|
56
69
|
/**
|
|
57
70
|
* Records one visit to a tracking link.
|
|
58
71
|
*
|
|
@@ -33,6 +33,15 @@ import { outreachEnrollmentLink, outreachOrgCollection, readStoredOutreachEnroll
|
|
|
33
33
|
}
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* When the email that carried a link was sent: the member's answer it was in
|
|
38
|
+
* (AGL-4022), else the step's email; `null` when it cannot be found.
|
|
39
|
+
*/ export function outreachLinkSentAtMs(enrollment, target) {
|
|
40
|
+
var _enrollment_stepRecords;
|
|
41
|
+
if (!target.answerKey) return outreachStepSentAtMs(enrollment, target.stepIndex);
|
|
42
|
+
const answer = ((_enrollment_stepRecords = enrollment.stepRecords) != null ? _enrollment_stepRecords : []).find((record)=>(record == null ? void 0 : record.kind) === 'manual_reply' && record.replyKey === target.answerKey);
|
|
43
|
+
return typeof (answer == null ? void 0 : answer.atMs) === 'number' && Number.isFinite(answer.atMs) ? answer.atMs : null;
|
|
44
|
+
}
|
|
36
45
|
/**
|
|
37
46
|
* Records one visit to a tracking link.
|
|
38
47
|
*
|
|
@@ -73,7 +82,7 @@ import { outreachEnrollmentLink, outreachOrgCollection, readStoredOutreachEnroll
|
|
|
73
82
|
enrollment: null
|
|
74
83
|
};
|
|
75
84
|
}
|
|
76
|
-
const sent =
|
|
85
|
+
const sent = outreachLinkSentAtMs(enrollment, input.target);
|
|
77
86
|
const judgement = judgeOutreachClick({
|
|
78
87
|
method: input.method,
|
|
79
88
|
userAgent: input.userAgent,
|
|
@@ -133,7 +142,8 @@ import { outreachEnrollmentLink, outreachOrgCollection, readStoredOutreachEnroll
|
|
|
133
142
|
url: key,
|
|
134
143
|
stepIndex,
|
|
135
144
|
human: judgement.human,
|
|
136
|
-
machineReason: judgement.machineReason
|
|
145
|
+
machineReason: judgement.machineReason,
|
|
146
|
+
answer: Boolean(input.target.answerKey)
|
|
137
147
|
}));
|
|
138
148
|
}
|
|
139
149
|
return _extends({}, judgement, {
|
|
@@ -144,13 +154,15 @@ import { outreachEnrollmentLink, outreachOrgCollection, readStoredOutreachEnroll
|
|
|
144
154
|
});
|
|
145
155
|
});
|
|
146
156
|
if (!outcome.enrollment) return outcome;
|
|
157
|
+
// An answer's link is the person's, never the sequence's (AGL-4022).
|
|
158
|
+
const fromAnswer = Boolean(input.target.answerKey);
|
|
147
159
|
await Promise.all([
|
|
148
|
-
bumpSequenceStats(firestore, outcome, {
|
|
160
|
+
fromAnswer ? null : bumpSequenceStats(firestore, outcome, {
|
|
149
161
|
orgId,
|
|
150
162
|
sequenceId: outcome.enrollment.sequenceId,
|
|
151
163
|
nowMs
|
|
152
164
|
}),
|
|
153
|
-
outcome.human ? bumpLinkRollup(firestore, {
|
|
165
|
+
outcome.human && !fromAnswer ? bumpLinkRollup(firestore, {
|
|
154
166
|
orgId,
|
|
155
167
|
sequenceId: outcome.enrollment.sequenceId,
|
|
156
168
|
url
|
|
@@ -159,7 +171,8 @@ import { outreachEnrollmentLink, outreachOrgCollection, readStoredOutreachEnroll
|
|
|
159
171
|
orgId,
|
|
160
172
|
enrollment: outcome.enrollment,
|
|
161
173
|
url,
|
|
162
|
-
nowMs
|
|
174
|
+
nowMs,
|
|
175
|
+
fromAnswer
|
|
163
176
|
}) : null
|
|
164
177
|
]);
|
|
165
178
|
return outcome;
|
|
@@ -266,7 +279,7 @@ import { outreachEnrollmentLink, outreachOrgCollection, readStoredOutreachEnroll
|
|
|
266
279
|
sourcePluginId: OUTREACH_PLUGIN_ID,
|
|
267
280
|
kind: 'note',
|
|
268
281
|
atMs: input.nowMs,
|
|
269
|
-
body:
|
|
282
|
+
body: `${input.fromAnswer ? 'Followed a link in an answer to their reply' : 'Followed a link in a sequence email'}: ${(_sendLinkKey = sendLinkKey(input.url)) != null ? _sendLinkKey : input.url}`,
|
|
270
283
|
byUid: '',
|
|
271
284
|
byName: null
|
|
272
285
|
});
|