@aglyn/plugins-outreach 1.0.0-beta.285 → 1.0.0-beta.287
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 +14 -14
- package/src/lib/constants/bundle-common.d.ts +7 -0
- package/src/lib/constants/bundle-common.js +18 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/engine/possible-reply.d.ts +45 -0
- package/src/lib/engine/possible-reply.js +61 -0
- package/src/lib/engine/possible-reply.js.map +1 -0
- package/src/lib/mailboxes/mailbox-gate.js +4 -2
- package/src/lib/mailboxes/mailbox-gate.js.map +1 -1
- package/src/lib/routes/route-gate.js +5 -2
- package/src/lib/routes/route-gate.js.map +1 -1
- package/src/lib/runtime/sync-job.d.ts +2 -0
- package/src/lib/runtime/sync-job.js +87 -3
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/transfer/sequences-package.server.js +3 -2
- package/src/lib/transfer/sequences-package.server.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-outreach",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.287",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.287",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.287",
|
|
30
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.287",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.287",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.287",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.287",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.287",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.287",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.287",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.287",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.287",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.287",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.287",
|
|
41
41
|
"@swc/helpers": "0.5.23"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -30,6 +30,13 @@ export declare const OUTREACH_PLUGIN_ID = "outreach";
|
|
|
30
30
|
* read the same string off the member's stamped `resolvedPermissions`.
|
|
31
31
|
*/
|
|
32
32
|
export declare const OUTREACH_USE_PERMISSION = "outreach.use";
|
|
33
|
+
/**
|
|
34
|
+
* "Manage everyone's Sequences" (AGL-4047): change, send from and pause a
|
|
35
|
+
* mailbox or sequence someone else connected, and set up link domains. It
|
|
36
|
+
* asked whether the member was the workspace's owner or an admin; its default
|
|
37
|
+
* is exactly those roles (owner on the admin tier).
|
|
38
|
+
*/
|
|
39
|
+
export declare const OUTREACH_MANAGE_ALL_PERMISSION = "outreach.manageAll";
|
|
33
40
|
/**
|
|
34
41
|
* This plugin's declared permissions (AGL-435 registry). Registered by both
|
|
35
42
|
* halves, so the console gate and a route's resolution agree.
|
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
* per-member override stores it by this exact string. The Firestore rules
|
|
27
27
|
* read the same string off the member's stamped `resolvedPermissions`.
|
|
28
28
|
*/ export const OUTREACH_USE_PERMISSION = 'outreach.use';
|
|
29
|
+
/**
|
|
30
|
+
* "Manage everyone's Sequences" (AGL-4047): change, send from and pause a
|
|
31
|
+
* mailbox or sequence someone else connected, and set up link domains. It
|
|
32
|
+
* asked whether the member was the workspace's owner or an admin; its default
|
|
33
|
+
* is exactly those roles (owner on the admin tier).
|
|
34
|
+
*/ export const OUTREACH_MANAGE_ALL_PERMISSION = 'outreach.manageAll';
|
|
29
35
|
/**
|
|
30
36
|
* This plugin's declared permissions (AGL-435 registry). Registered by both
|
|
31
37
|
* halves, so the console gate and a route's resolution agree.
|
|
@@ -50,6 +56,18 @@
|
|
|
50
56
|
viewer: false
|
|
51
57
|
},
|
|
52
58
|
groupLabel: 'Sequences'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: OUTREACH_MANAGE_ALL_PERMISSION,
|
|
62
|
+
pluginId: OUTREACH_PLUGIN_ID,
|
|
63
|
+
label: 'Manage everyone’s Sequences',
|
|
64
|
+
description: 'Change, send from and pause a mailbox or sequence someone else connected, and set up link domains.',
|
|
65
|
+
defaults: {
|
|
66
|
+
admin: true,
|
|
67
|
+
editor: false,
|
|
68
|
+
viewer: false
|
|
69
|
+
},
|
|
70
|
+
groupLabel: 'Sequences'
|
|
53
71
|
}
|
|
54
72
|
];
|
|
55
73
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/constants/bundle-common.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 { PluginPermission } from '@aglyn/aglyn/plugin-manager/plugin-permissions'\n\n/**\n * The persisted plugin id (AGL-2974): stored in `org.enabledPlugins`, named in\n * `plugins.config.json` and `FIRST_PARTY_PLUGINS`, and the owner of every\n * `outreach/*` API route. Renaming it is a data change.\n */\nexport const OUTREACH_PLUGIN_ID = 'outreach'\n\n/**\n * The permission that opens Sequences.\n *\n * Dotted like the catalog's keys but declared by this plugin, so the console\n * answers it from the resolved permission map and a custom role or a\n * per-member override stores it by this exact string. The Firestore rules\n * read the same string off the member's stamped `resolvedPermissions`.\n */\nexport const OUTREACH_USE_PERMISSION = 'outreach.use'\n\n/**\n * This plugin's declared permissions (AGL-435 registry). Registered by both\n * halves, so the console gate and a route's resolution agree.\n *\n * Owners and admins hold it by default — `owner` resolves onto the admin\n * tier — and editors and viewers do not: a sequence sends mail as a person\n * from their own mailbox, which is a decision for the people who run the\n * workspace to hand out, one member or one custom role at a time.\n *\n * The KEY is `outreach.use` and the LABEL reads \"Use Sequences\" (AGL-3199):\n * the key is stored on custom roles and stamped on members, and the label is\n * the only half anyone reads.\n */\nexport const OUTREACH_PERMISSIONS: readonly PluginPermission[] = [\n {\n key: OUTREACH_USE_PERMISSION,\n pluginId: OUTREACH_PLUGIN_ID,\n label: 'Use Sequences',\n description:\n 'Open Sequences, and work its email sequences and connected mailboxes.',\n defaults: { admin: true, editor: false, viewer: false },\n groupLabel: 'Sequences',\n },\n]\n"],"names":["OUTREACH_PLUGIN_ID","OUTREACH_USE_PERMISSION","OUTREACH_PERMISSIONS","key","pluginId","label","description","defaults","admin","editor","viewer","groupLabel"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;CAIC,GACD,OAAO,MAAMA,qBAAqB,WAAU;AAE5C;;;;;;;CAOC,GACD,OAAO,MAAMC,0BAA0B,eAAc;AAErD;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,uBAAoD;IAC/D;QACEC,KAAKF;QACLG,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/constants/bundle-common.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 { PluginPermission } from '@aglyn/aglyn/plugin-manager/plugin-permissions'\n\n/**\n * The persisted plugin id (AGL-2974): stored in `org.enabledPlugins`, named in\n * `plugins.config.json` and `FIRST_PARTY_PLUGINS`, and the owner of every\n * `outreach/*` API route. Renaming it is a data change.\n */\nexport const OUTREACH_PLUGIN_ID = 'outreach'\n\n/**\n * The permission that opens Sequences.\n *\n * Dotted like the catalog's keys but declared by this plugin, so the console\n * answers it from the resolved permission map and a custom role or a\n * per-member override stores it by this exact string. The Firestore rules\n * read the same string off the member's stamped `resolvedPermissions`.\n */\nexport const OUTREACH_USE_PERMISSION = 'outreach.use'\n\n/**\n * \"Manage everyone's Sequences\" (AGL-4047): change, send from and pause a\n * mailbox or sequence someone else connected, and set up link domains. It\n * asked whether the member was the workspace's owner or an admin; its default\n * is exactly those roles (owner on the admin tier).\n */\nexport const OUTREACH_MANAGE_ALL_PERMISSION = 'outreach.manageAll'\n\n/**\n * This plugin's declared permissions (AGL-435 registry). Registered by both\n * halves, so the console gate and a route's resolution agree.\n *\n * Owners and admins hold it by default — `owner` resolves onto the admin\n * tier — and editors and viewers do not: a sequence sends mail as a person\n * from their own mailbox, which is a decision for the people who run the\n * workspace to hand out, one member or one custom role at a time.\n *\n * The KEY is `outreach.use` and the LABEL reads \"Use Sequences\" (AGL-3199):\n * the key is stored on custom roles and stamped on members, and the label is\n * the only half anyone reads.\n */\nexport const OUTREACH_PERMISSIONS: readonly PluginPermission[] = [\n {\n key: OUTREACH_USE_PERMISSION,\n pluginId: OUTREACH_PLUGIN_ID,\n label: 'Use Sequences',\n description:\n 'Open Sequences, and work its email sequences and connected mailboxes.',\n defaults: { admin: true, editor: false, viewer: false },\n groupLabel: 'Sequences',\n },\n {\n key: OUTREACH_MANAGE_ALL_PERMISSION,\n pluginId: OUTREACH_PLUGIN_ID,\n label: 'Manage everyone’s Sequences',\n description:\n 'Change, send from and pause a mailbox or sequence someone else connected, and set up link domains.',\n defaults: { admin: true, editor: false, viewer: false },\n groupLabel: 'Sequences',\n },\n]\n"],"names":["OUTREACH_PLUGIN_ID","OUTREACH_USE_PERMISSION","OUTREACH_MANAGE_ALL_PERMISSION","OUTREACH_PERMISSIONS","key","pluginId","label","description","defaults","admin","editor","viewer","groupLabel"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;CAIC,GACD,OAAO,MAAMA,qBAAqB,WAAU;AAE5C;;;;;;;CAOC,GACD,OAAO,MAAMC,0BAA0B,eAAc;AAErD;;;;;CAKC,GACD,OAAO,MAAMC,iCAAiC,qBAAoB;AAElE;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,uBAAoD;IAC/D;QACEC,KAAKH;QACLI,UAAUL;QACVM,OAAO;QACPC,aACE;QACFC,UAAU;YAAEC,OAAO;YAAMC,QAAQ;YAAOC,QAAQ;QAAM;QACtDC,YAAY;IACd;IACA;QACER,KAAKF;QACLG,UAAUL;QACVM,OAAO;QACPC,aACE;QACFC,UAAU;YAAEC,OAAO;YAAMC,QAAQ;YAAOC,QAAQ;QAAM;QACtDC,YAAY;IACd;CACD,CAAA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
export type OutreachPossibleReplyReason = 'other_inbox' | 'colleague';
|
|
18
|
+
/** What the matcher reads of an enrollment. */
|
|
19
|
+
export interface OutreachPossibleReplyCandidate {
|
|
20
|
+
email: string;
|
|
21
|
+
mailboxId: string | null;
|
|
22
|
+
lastSentAtMs?: number | null;
|
|
23
|
+
}
|
|
24
|
+
export interface OutreachPossibleReplyMatch<T extends OutreachPossibleReplyCandidate> {
|
|
25
|
+
enrollment: T;
|
|
26
|
+
reason: OutreachPossibleReplyReason;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The enrollment a message from `sender`, received by `mailboxId`, may be
|
|
30
|
+
* answering, or `null` — see the module note. The enrollment written to most
|
|
31
|
+
* recently wins when several match.
|
|
32
|
+
*/
|
|
33
|
+
export declare function matchOutreachPossibleReply<T extends OutreachPossibleReplyCandidate>(sender: unknown, input: {
|
|
34
|
+
mailboxId: string;
|
|
35
|
+
enrollments: readonly T[];
|
|
36
|
+
}): OutreachPossibleReplyMatch<T> | null;
|
|
37
|
+
/** The task's title: who wrote, and from which company domain. */
|
|
38
|
+
export declare function outreachPossibleReplyTitle(senderName: string, senderEmail: string): string;
|
|
39
|
+
/** The task's note: what it is about, and that nothing was stopped. */
|
|
40
|
+
export declare function outreachPossibleReplyNote(input: {
|
|
41
|
+
reason: OutreachPossibleReplyReason;
|
|
42
|
+
enrolledName: string;
|
|
43
|
+
receivedBy: string;
|
|
44
|
+
excerpt: string;
|
|
45
|
+
}): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { normalizeContactEmail } from "@aglyn/aglyn/app-utils/contacts";
|
|
17
|
+
import { isPublicMailboxDomain } from "@aglyn/aglyn/app-utils/crm";
|
|
18
|
+
import { outreachEmailDomain } from "./do-not-contact-domain.js";
|
|
19
|
+
/**
|
|
20
|
+
* The enrollment a message from `sender`, received by `mailboxId`, may be
|
|
21
|
+
* answering, or `null` — see the module note. The enrollment written to most
|
|
22
|
+
* recently wins when several match.
|
|
23
|
+
*/ export function matchOutreachPossibleReply(sender, input) {
|
|
24
|
+
const address = normalizeContactEmail(sender);
|
|
25
|
+
if (!address) return null;
|
|
26
|
+
const latest = (list)=>list.reduce((best, next)=>{
|
|
27
|
+
var _next_lastSentAtMs, _best_lastSentAtMs;
|
|
28
|
+
return !best || ((_next_lastSentAtMs = next.lastSentAtMs) != null ? _next_lastSentAtMs : 0) > ((_best_lastSentAtMs = best.lastSentAtMs) != null ? _best_lastSentAtMs : 0) ? next : best;
|
|
29
|
+
}, null);
|
|
30
|
+
const same = input.enrollments.filter((enrollment)=>normalizeContactEmail(enrollment.email) === address);
|
|
31
|
+
// The receiving mailbox's own enrollment is the sync's step 4, not this.
|
|
32
|
+
if (same.some((enrollment)=>enrollment.mailboxId === input.mailboxId)) return null;
|
|
33
|
+
const elsewhere = latest(same);
|
|
34
|
+
if (elsewhere) return {
|
|
35
|
+
enrollment: elsewhere,
|
|
36
|
+
reason: 'other_inbox'
|
|
37
|
+
};
|
|
38
|
+
const domain = outreachEmailDomain(address);
|
|
39
|
+
if (!domain || isPublicMailboxDomain(domain)) return null;
|
|
40
|
+
const colleague = latest(input.enrollments.filter((enrollment)=>outreachEmailDomain(enrollment.email) === domain));
|
|
41
|
+
return colleague ? {
|
|
42
|
+
enrollment: colleague,
|
|
43
|
+
reason: 'colleague'
|
|
44
|
+
} : null;
|
|
45
|
+
}
|
|
46
|
+
/** The task's title: who wrote, and from which company domain. */ export function outreachPossibleReplyTitle(senderName, senderEmail) {
|
|
47
|
+
const domain = outreachEmailDomain(senderEmail);
|
|
48
|
+
const who = senderName.trim() || senderEmail;
|
|
49
|
+
return domain ? `Possible reply from ${who} (${domain})` : `Possible reply from ${who}`;
|
|
50
|
+
}
|
|
51
|
+
/** The task's note: what it is about, and that nothing was stopped. */ export function outreachPossibleReplyNote(input) {
|
|
52
|
+
const why = input.reason === 'other_inbox' ? `${input.enrolledName} wrote to ${input.receivedBy}, not the mailbox writing to them.` : `A colleague of ${input.enrolledName} wrote to ${input.receivedBy}.`;
|
|
53
|
+
return [
|
|
54
|
+
why,
|
|
55
|
+
'The sequence keeps going until someone reads this and decides.',
|
|
56
|
+
'',
|
|
57
|
+
input.excerpt
|
|
58
|
+
].join('\n').trim().slice(0, 800);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=possible-reply.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/possible-reply.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 { outreachEmailDomain } from './do-not-contact-domain'\n\n/*==========================================\n * POSSIBLE REPLIES (AGL-4088).\n *\n * The sync job already treats mail from an enrolled address as that\n * enrollment's reply, in its thread or in one of its own. Two answers slip\n * past it, and this names them:\n *\n * - `other_inbox` — the person wrote to a different one of the\n * organization's mailboxes than the one writing to them;\n * - `colleague` — somebody else at the person's company wrote, the way an\n * owner hands a cold email to the marketing lead who answers it.\n *\n * Neither stops anything. A colleague's message can as easily be a vendor's\n * newsletter or an invoice, so a person reads it: the runtime files a task,\n * \"Possible reply from …\", on the enrollment's record, and the sequence\n * keeps its course until someone decides. A public mailbox domain (gmail.com\n * and the rest) is never a company, so it never matches as a colleague.\n *==========================================*/\n\nexport type OutreachPossibleReplyReason = 'other_inbox' | 'colleague'\n\n/** What the matcher reads of an enrollment. */\nexport interface OutreachPossibleReplyCandidate {\n email: string\n mailboxId: string | null\n lastSentAtMs?: number | null\n}\n\nexport interface OutreachPossibleReplyMatch<T extends OutreachPossibleReplyCandidate> {\n enrollment: T\n reason: OutreachPossibleReplyReason\n}\n\n/**\n * The enrollment a message from `sender`, received by `mailboxId`, may be\n * answering, or `null` — see the module note. The enrollment written to most\n * recently wins when several match.\n */\nexport function matchOutreachPossibleReply<T extends OutreachPossibleReplyCandidate>(\n sender: unknown,\n input: { mailboxId: string; enrollments: readonly T[] },\n): OutreachPossibleReplyMatch<T> | null {\n const address = normalizeContactEmail(sender)\n if (!address) return null\n const latest = (list: T[]) =>\n list.reduce<T | null>((best, next) => (!best || (next.lastSentAtMs ?? 0) > (best.lastSentAtMs ?? 0) ? next : best), null)\n\n const same = input.enrollments.filter((enrollment) => normalizeContactEmail(enrollment.email) === address)\n // The receiving mailbox's own enrollment is the sync's step 4, not this.\n if (same.some((enrollment) => enrollment.mailboxId === input.mailboxId)) return null\n const elsewhere = latest(same)\n if (elsewhere) return { enrollment: elsewhere, reason: 'other_inbox' }\n\n const domain = outreachEmailDomain(address)\n if (!domain || isPublicMailboxDomain(domain)) return null\n const colleague = latest(input.enrollments.filter((enrollment) => outreachEmailDomain(enrollment.email) === domain))\n return colleague ? { enrollment: colleague, reason: 'colleague' } : null\n}\n\n/** The task's title: who wrote, and from which company domain. */\nexport function outreachPossibleReplyTitle(senderName: string, senderEmail: string): string {\n const domain = outreachEmailDomain(senderEmail)\n const who = senderName.trim() || senderEmail\n return domain ? `Possible reply from ${who} (${domain})` : `Possible reply from ${who}`\n}\n\n/** The task's note: what it is about, and that nothing was stopped. */\nexport function outreachPossibleReplyNote(input: {\n reason: OutreachPossibleReplyReason\n enrolledName: string\n receivedBy: string\n excerpt: string\n}): string {\n const why =\n input.reason === 'other_inbox'\n ? `${input.enrolledName} wrote to ${input.receivedBy}, not the mailbox writing to them.`\n : `A colleague of ${input.enrolledName} wrote to ${input.receivedBy}.`\n return [why, 'The sequence keeps going until someone reads this and decides.', '', input.excerpt]\n .join('\\n')\n .trim()\n .slice(0, 800)\n}\n"],"names":["normalizeContactEmail","isPublicMailboxDomain","outreachEmailDomain","matchOutreachPossibleReply","sender","input","address","latest","list","reduce","best","next","lastSentAtMs","same","enrollments","filter","enrollment","email","some","mailboxId","elsewhere","reason","domain","colleague","outreachPossibleReplyTitle","senderName","senderEmail","who","trim","outreachPossibleReplyNote","why","enrolledName","receivedBy","excerpt","join","slice"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,qBAAqB,QAAQ,kCAAiC;AACvE,SAASC,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,mBAAmB,QAAQ,6BAAyB;AAmC7D;;;;CAIC,GACD,OAAO,SAASC,2BACdC,MAAe,EACfC,KAAuD;IAEvD,MAAMC,UAAUN,sBAAsBI;IACtC,IAAI,CAACE,SAAS,OAAO;IACrB,MAAMC,SAAS,CAACC,OACdA,KAAKC,MAAM,CAAW,CAACC,MAAMC;gBAAoBA,oBAA2BD;mBAArC,CAACA,QAAQ,EAACC,qBAAAA,KAAKC,YAAY,YAAjBD,qBAAqB,OAAMD,qBAAAA,KAAKE,YAAY,YAAjBF,qBAAqB,KAAKC,OAAOD;WAAO;IAEtH,MAAMG,OAAOR,MAAMS,WAAW,CAACC,MAAM,CAAC,CAACC,aAAehB,sBAAsBgB,WAAWC,KAAK,MAAMX;IAClG,yEAAyE;IACzE,IAAIO,KAAKK,IAAI,CAAC,CAACF,aAAeA,WAAWG,SAAS,KAAKd,MAAMc,SAAS,GAAG,OAAO;IAChF,MAAMC,YAAYb,OAAOM;IACzB,IAAIO,WAAW,OAAO;QAAEJ,YAAYI;QAAWC,QAAQ;IAAc;IAErE,MAAMC,SAASpB,oBAAoBI;IACnC,IAAI,CAACgB,UAAUrB,sBAAsBqB,SAAS,OAAO;IACrD,MAAMC,YAAYhB,OAAOF,MAAMS,WAAW,CAACC,MAAM,CAAC,CAACC,aAAed,oBAAoBc,WAAWC,KAAK,MAAMK;IAC5G,OAAOC,YAAY;QAAEP,YAAYO;QAAWF,QAAQ;IAAY,IAAI;AACtE;AAEA,gEAAgE,GAChE,OAAO,SAASG,2BAA2BC,UAAkB,EAAEC,WAAmB;IAChF,MAAMJ,SAASpB,oBAAoBwB;IACnC,MAAMC,MAAMF,WAAWG,IAAI,MAAMF;IACjC,OAAOJ,SAAS,CAAC,oBAAoB,EAAEK,IAAI,EAAE,EAAEL,OAAO,CAAC,CAAC,GAAG,CAAC,oBAAoB,EAAEK,KAAK;AACzF;AAEA,qEAAqE,GACrE,OAAO,SAASE,0BAA0BxB,KAKzC;IACC,MAAMyB,MACJzB,MAAMgB,MAAM,KAAK,gBACb,GAAGhB,MAAM0B,YAAY,CAAC,UAAU,EAAE1B,MAAM2B,UAAU,CAAC,kCAAkC,CAAC,GACtF,CAAC,eAAe,EAAE3B,MAAM0B,YAAY,CAAC,UAAU,EAAE1B,MAAM2B,UAAU,CAAC,CAAC,CAAC;IAC1E,OAAO;QAACF;QAAK;QAAkE;QAAIzB,MAAM4B,OAAO;KAAC,CAC9FC,IAAI,CAAC,MACLN,IAAI,GACJO,KAAK,CAAC,GAAG;AACd"}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/ import { checkEntitlement } from "@aglyn/aglyn/server";
|
|
17
17
|
import { isEmailVerified, isImpersonationSession } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
18
18
|
import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
19
|
-
import { OUTREACH_USE_PERMISSION } from "../constants/bundle-common.js";
|
|
19
|
+
import { OUTREACH_MANAGE_ALL_PERMISSION, OUTREACH_USE_PERMISSION } from "../constants/bundle-common.js";
|
|
20
20
|
/** A refusal in the one shape every mailbox route answers with. */ export function refusal(status, reason, error) {
|
|
21
21
|
return Response.json({
|
|
22
22
|
error,
|
|
@@ -91,7 +91,9 @@ export async function outreachMemberGate(request, rawOrgId, deps) {
|
|
|
91
91
|
staff,
|
|
92
92
|
orgId,
|
|
93
93
|
org,
|
|
94
|
-
|
|
94
|
+
// "Manage everyone's Sequences" (AGL-4047), whose default is the owner
|
|
95
|
+
// and admin `isOwner` named.
|
|
96
|
+
isOrgAdmin: membership.permissions[OUTREACH_MANAGE_ALL_PERMISSION] === true
|
|
95
97
|
};
|
|
96
98
|
}
|
|
97
99
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/mailbox-gate.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 { checkEntitlement } from '@aglyn/aglyn/server'\nimport {\n isEmailVerified,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport type { DecodedIdToken } from 'firebase-admin/auth'\nimport { OUTREACH_USE_PERMISSION } from '../constants/bundle-common'\nimport type { OutreachApiRefusalReason } from './mailbox-api'\n\n/**\n * THE GATE EVERY AUTHENTICATED MAILBOX ROUTE CLIMBS (AGL-2978).\n *\n * The console dispatcher has already refused a request while\n * `release_outreach` is off for the organization it names, an unverified\n * account's token, a lockdown with no org in scope, and a caller over the\n * console rate limit. What it cannot know is anything about the member, so\n * each rung below is this plugin's:\n *\n * 401 no bearer token, or one the verifier refused\n * 403 the account's address is unverified (an impersonation session is exempt)\n * 400 the request did not name an organization\n * 403 not a member of it, a site collaborator without org-wide reach, or\n * without `outreach.use`\n * 403 the organization does not hold the `outreach` entitlement\n * 423 lockdown, now with the organization and the account in scope\n *\n * `resolveOrgPermissions` is always given the concrete org id. Called without\n * one it resolves a caller with no org at all as that future org's owner,\n * which is right for a fresh signup and wrong for every question asked here.\n */\n\nexport interface OutreachGateDeps {\n verifyIdToken(idToken: string): Promise<DecodedIdToken>\n resolveOrgPermissions(\n uid: string,\n context: { orgId: string },\n ): Promise<{\n orgId: string | null\n /** `null` when the account is not on the organization's roster. */\n role: string | null\n isOwner: boolean\n orgWide: boolean\n permissions: Partial<Record<string, boolean | undefined>>\n }>\n readOrg(orgId: string): Promise<Record<string, unknown> | null>\n lockdownRefusal(options: {\n request: Request\n staff: boolean\n uid: string\n org: Record<string, unknown>\n }): Promise<Response | null>\n}\n\nexport interface OutreachGateContext {\n uid: string\n /** The account's sign-in address, lower-cased, when the token carries one. */\n email: string | null\n /** The account's name, when the token carries one. */\n name: string | null\n staff: boolean\n orgId: string\n org: Record<string, unknown>\n /** An owner or admin of the organization, who may manage every mailbox. */\n isOrgAdmin: boolean\n}\n\n/** A refusal in the one shape every mailbox route answers with. */\nexport function refusal(status: number, reason: OutreachApiRefusalReason, error: string): Response {\n return Response.json({ error, reason }, { status, headers: { 'Cache-Control': 'no-store' } })\n}\n\n/** An organization id as a request may name one: a plain document id. */\nconst ORG_ID = /^[A-Za-z0-9_-]{1,128}$/\n\nexport function readOrgId(value: unknown): string | null {\n const orgId = typeof value === 'string' ? value.trim() : ''\n return ORG_ID.test(orgId) ? orgId : null\n}\n\nfunction bearer(request: Request): string | null {\n const authorization = request.headers.get('authorization') ?? ''\n return authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : null\n}\n\nexport async function outreachMemberGate(\n request: Request,\n rawOrgId: unknown,\n deps: OutreachGateDeps,\n): Promise<Response | OutreachGateContext> {\n const token = bearer(request)\n if (!token) return refusal(401, 'unauthenticated', 'Sign in to manage your connected mailboxes.')\n let decoded: DecodedIdToken\n try {\n decoded = await deps.verifyIdToken(token)\n } catch (error) {\n // A refused credential is the caller's; an Auth outage is ours and\n // propagates, so it answers 500 instead of \"sign in again\".\n if (invalidIdTokenResponse(error)) {\n return refusal(401, 'unauthenticated', 'Your sign-in could not be confirmed. Sign in again.')\n }\n throw error\n }\n if (!isEmailVerified(decoded) && !isImpersonationSession(decoded)) {\n return refusal(403, 'email-unverified', 'Verify your email address before connecting a mailbox.')\n }\n\n const orgId = readOrgId(rawOrgId)\n if (!orgId) return refusal(400, 'org-required', 'Open an organization before managing its mailboxes.')\n\n const membership = await deps.resolveOrgPermissions(decoded.uid, { orgId })\n // A targeted org the account is not on answers with that org's id and no\n // role — as does a lookup that failed closed — so the role is the test.\n if (!membership.role || membership.orgId !== orgId) {\n return refusal(403, 'not-a-member', 'You are not a member of that organization.')\n }\n if (!membership.orgWide) {\n return refusal(403, 'not-org-wide', 'Sequences covers the whole organization, and your access is to particular sites.')\n }\n if (membership.permissions[OUTREACH_USE_PERMISSION] !== true) {\n return refusal(403, 'permission', 'Your role does not include Use Sequences.')\n }\n\n const org = (await deps.readOrg(orgId)) ?? {}\n if (!checkEntitlement(org, 'outreach')) {\n return refusal(403, 'entitlement', \"Sequences isn't available to this workspace yet.\")\n }\n const staff = decoded['staff'] === true\n const locked = await deps.lockdownRefusal({ request, staff, uid: decoded.uid, org })\n if (locked) return locked\n\n return {\n uid: decoded.uid,\n email: typeof decoded.email === 'string' ? decoded.email.trim().toLowerCase() : null,\n name: typeof decoded['name'] === 'string' ? String(decoded['name']) : null,\n staff,\n orgId,\n org,\n isOrgAdmin: membership.
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/mailbox-gate.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 { checkEntitlement } from '@aglyn/aglyn/server'\nimport {\n isEmailVerified,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport type { DecodedIdToken } from 'firebase-admin/auth'\nimport { OUTREACH_MANAGE_ALL_PERMISSION, OUTREACH_USE_PERMISSION } from '../constants/bundle-common'\nimport type { OutreachApiRefusalReason } from './mailbox-api'\n\n/**\n * THE GATE EVERY AUTHENTICATED MAILBOX ROUTE CLIMBS (AGL-2978).\n *\n * The console dispatcher has already refused a request while\n * `release_outreach` is off for the organization it names, an unverified\n * account's token, a lockdown with no org in scope, and a caller over the\n * console rate limit. What it cannot know is anything about the member, so\n * each rung below is this plugin's:\n *\n * 401 no bearer token, or one the verifier refused\n * 403 the account's address is unverified (an impersonation session is exempt)\n * 400 the request did not name an organization\n * 403 not a member of it, a site collaborator without org-wide reach, or\n * without `outreach.use`\n * 403 the organization does not hold the `outreach` entitlement\n * 423 lockdown, now with the organization and the account in scope\n *\n * `resolveOrgPermissions` is always given the concrete org id. Called without\n * one it resolves a caller with no org at all as that future org's owner,\n * which is right for a fresh signup and wrong for every question asked here.\n */\n\nexport interface OutreachGateDeps {\n verifyIdToken(idToken: string): Promise<DecodedIdToken>\n resolveOrgPermissions(\n uid: string,\n context: { orgId: string },\n ): Promise<{\n orgId: string | null\n /** `null` when the account is not on the organization's roster. */\n role: string | null\n isOwner: boolean\n orgWide: boolean\n permissions: Partial<Record<string, boolean | undefined>>\n }>\n readOrg(orgId: string): Promise<Record<string, unknown> | null>\n lockdownRefusal(options: {\n request: Request\n staff: boolean\n uid: string\n org: Record<string, unknown>\n }): Promise<Response | null>\n}\n\nexport interface OutreachGateContext {\n uid: string\n /** The account's sign-in address, lower-cased, when the token carries one. */\n email: string | null\n /** The account's name, when the token carries one. */\n name: string | null\n staff: boolean\n orgId: string\n org: Record<string, unknown>\n /** An owner or admin of the organization, who may manage every mailbox. */\n isOrgAdmin: boolean\n}\n\n/** A refusal in the one shape every mailbox route answers with. */\nexport function refusal(status: number, reason: OutreachApiRefusalReason, error: string): Response {\n return Response.json({ error, reason }, { status, headers: { 'Cache-Control': 'no-store' } })\n}\n\n/** An organization id as a request may name one: a plain document id. */\nconst ORG_ID = /^[A-Za-z0-9_-]{1,128}$/\n\nexport function readOrgId(value: unknown): string | null {\n const orgId = typeof value === 'string' ? value.trim() : ''\n return ORG_ID.test(orgId) ? orgId : null\n}\n\nfunction bearer(request: Request): string | null {\n const authorization = request.headers.get('authorization') ?? ''\n return authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : null\n}\n\nexport async function outreachMemberGate(\n request: Request,\n rawOrgId: unknown,\n deps: OutreachGateDeps,\n): Promise<Response | OutreachGateContext> {\n const token = bearer(request)\n if (!token) return refusal(401, 'unauthenticated', 'Sign in to manage your connected mailboxes.')\n let decoded: DecodedIdToken\n try {\n decoded = await deps.verifyIdToken(token)\n } catch (error) {\n // A refused credential is the caller's; an Auth outage is ours and\n // propagates, so it answers 500 instead of \"sign in again\".\n if (invalidIdTokenResponse(error)) {\n return refusal(401, 'unauthenticated', 'Your sign-in could not be confirmed. Sign in again.')\n }\n throw error\n }\n if (!isEmailVerified(decoded) && !isImpersonationSession(decoded)) {\n return refusal(403, 'email-unverified', 'Verify your email address before connecting a mailbox.')\n }\n\n const orgId = readOrgId(rawOrgId)\n if (!orgId) return refusal(400, 'org-required', 'Open an organization before managing its mailboxes.')\n\n const membership = await deps.resolveOrgPermissions(decoded.uid, { orgId })\n // A targeted org the account is not on answers with that org's id and no\n // role — as does a lookup that failed closed — so the role is the test.\n if (!membership.role || membership.orgId !== orgId) {\n return refusal(403, 'not-a-member', 'You are not a member of that organization.')\n }\n if (!membership.orgWide) {\n return refusal(403, 'not-org-wide', 'Sequences covers the whole organization, and your access is to particular sites.')\n }\n if (membership.permissions[OUTREACH_USE_PERMISSION] !== true) {\n return refusal(403, 'permission', 'Your role does not include Use Sequences.')\n }\n\n const org = (await deps.readOrg(orgId)) ?? {}\n if (!checkEntitlement(org, 'outreach')) {\n return refusal(403, 'entitlement', \"Sequences isn't available to this workspace yet.\")\n }\n const staff = decoded['staff'] === true\n const locked = await deps.lockdownRefusal({ request, staff, uid: decoded.uid, org })\n if (locked) return locked\n\n return {\n uid: decoded.uid,\n email: typeof decoded.email === 'string' ? decoded.email.trim().toLowerCase() : null,\n name: typeof decoded['name'] === 'string' ? String(decoded['name']) : null,\n staff,\n orgId,\n org,\n // \"Manage everyone's Sequences\" (AGL-4047), whose default is the owner\n // and admin `isOwner` named.\n isOrgAdmin: membership.permissions[OUTREACH_MANAGE_ALL_PERMISSION] === true,\n }\n}\n"],"names":["checkEntitlement","isEmailVerified","isImpersonationSession","invalidIdTokenResponse","OUTREACH_MANAGE_ALL_PERMISSION","OUTREACH_USE_PERMISSION","refusal","status","reason","error","Response","json","headers","ORG_ID","readOrgId","value","orgId","trim","test","bearer","request","authorization","get","startsWith","slice","length","outreachMemberGate","rawOrgId","deps","token","decoded","verifyIdToken","membership","resolveOrgPermissions","uid","role","orgWide","permissions","org","readOrg","staff","locked","lockdownRefusal","email","toLowerCase","name","String","isOrgAdmin"],"mappings":"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"}
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
16
|
+
*/ import { OUTREACH_MANAGE_ALL_PERMISSION } from "../constants/bundle-common.js";
|
|
17
|
+
import { isEmailVerified, isImpersonationSession } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
17
18
|
import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
18
19
|
import { outreachEntitlementRefusal, outreachMembershipRefusal } from "../engine/outreach-access.js";
|
|
19
20
|
import { outreachRefusal, readOutreachDocumentId } from "./route-http.js";
|
|
@@ -69,7 +70,9 @@ export async function outreachRouteGate(request, rawOrgId, deps, extra = []) {
|
|
|
69
70
|
staff,
|
|
70
71
|
orgId,
|
|
71
72
|
org,
|
|
72
|
-
|
|
73
|
+
// "Manage everyone's Sequences" (AGL-4047), whose default is the owner
|
|
74
|
+
// and admin `isOwner` named.
|
|
75
|
+
isOrgAdmin: membership.permissions[OUTREACH_MANAGE_ALL_PERMISSION] === true
|
|
73
76
|
};
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/route-gate.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 {\n isEmailVerified,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport type { DecodedIdToken } from 'firebase-admin/auth'\nimport { outreachEntitlementRefusal, outreachMembershipRefusal } from '../engine/outreach-access'\nimport { outreachRefusal, readOutreachDocumentId } from './route-http'\n\n/**\n * THE GATE EVERY SETTINGS, SEQUENCE AND ENROLLMENT ROUTE CLIMBS (AGL-2980).\n *\n * The console dispatcher has already refused a request while\n * `release_outreach` is off for it, an unverified account's token, a\n * lockdown with no org in scope, and a caller over the console rate limit —\n * every route here is registered under the `outreach/` prefix, which is what\n * puts it behind those. What the dispatcher cannot know is anything about\n * the member, so each rung below is this plugin's:\n *\n * 401 no bearer token, or one the verifier refused\n * 403 the account's address is unverified (an impersonation session is exempt)\n * 400 the request did not name an organization\n * 403 not a member of it, a site collaborator without org-wide reach, or\n * without `outreach.use` — or without a permission the route adds\n * 403 the organization does not hold the `outreach` entitlement\n * 423 lockdown, now with the organization and the account in scope\n *\n * The reach is org-wide for the reason the rules give: a sequence enrolls\n * people from every site of the organization, so none of it is a fact\n * about the one site a collaborator was invited to.\n *\n * `resolveOrgPermissions` is always given the concrete org id. Called\n * without one it resolves a caller with no org at all as that future org's\n * owner, which is right for a fresh signup and wrong for every question\n * asked here.\n */\n\nexport interface OutreachRouteGateDeps {\n verifyIdToken(idToken: string): Promise<DecodedIdToken>\n resolveOrgPermissions(\n uid: string,\n context: { orgId: string },\n ): Promise<{\n orgId: string | null\n /** `null` when the account is not on the organization's roster. */\n role: string | null\n isOwner: boolean\n orgWide: boolean\n permissions: Partial<Record<string, boolean | undefined>>\n }>\n /**\n * Whether the member holds a CATALOG permission (`data.manage`) as the\n * org's roles resolve it. Asked separately because the map\n * `resolveOrgPermissions` answers carries the legacy keys and the plugins'\n * own, never a catalog key: read there, a catalog key refuses everyone.\n */\n holdsOrgCatalogPermission(uid: string, orgId: string, key: string): Promise<boolean>\n readOrg(orgId: string): Promise<Record<string, unknown> | null>\n lockdownRefusal(options: {\n request: Request\n staff: boolean\n uid: string\n org: Record<string, unknown>\n }): Promise<Response | null>\n}\n\n/** Who is asking, once the gate let them through. */\nexport interface OutreachRouteCaller {\n uid: string\n /** The account's sign-in address, lower-cased, when the token carries one. */\n email: string | null\n staff: boolean\n orgId: string\n /** The organization document the entitlement was read from. */\n org: Record<string, unknown>\n /** An owner or admin of the organization, who may send from any member's mailbox. */\n isOrgAdmin: boolean\n}\n\n/**\n * A catalog permission a route asks for beyond `outreach.use`, with its\n * refusal — `data.manage` for a route that reads the CRM's people, which\n * the contacts' own rules gate on.\n */\nexport interface OutreachRouteExtraPermission {\n key: string\n /** Why the route needs it, as the refusal says it. */\n refusal: string\n}\n\nfunction bearer(request: Request): string | null {\n const authorization = request.headers.get('authorization') ?? ''\n return authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : null\n}\n\nexport async function outreachRouteGate(\n request: Request,\n rawOrgId: unknown,\n deps: OutreachRouteGateDeps,\n extra: readonly OutreachRouteExtraPermission[] = [],\n): Promise<Response | OutreachRouteCaller> {\n const token = bearer(request)\n if (!token) return outreachRefusal(401, 'unauthenticated', 'Sign in to use Sequences.')\n let decoded: DecodedIdToken\n try {\n decoded = await deps.verifyIdToken(token)\n } catch (error) {\n // A refused credential is the caller's; an Auth outage is ours and\n // propagates, so it answers 500 instead of \"sign in again\".\n if (invalidIdTokenResponse(error)) {\n return outreachRefusal(401, 'unauthenticated', 'Your sign-in could not be confirmed. Sign in again.')\n }\n throw error\n }\n if (!isEmailVerified(decoded) && !isImpersonationSession(decoded)) {\n return outreachRefusal(403, 'email-unverified', 'Verify your email address before using Sequences.')\n }\n\n const orgId = readOutreachDocumentId(rawOrgId)\n if (!orgId) return outreachRefusal(400, 'org-required', 'Open an organization before using Sequences.')\n\n const membership = await deps.resolveOrgPermissions(decoded.uid, { orgId })\n const refused = outreachMembershipRefusal(membership, orgId)\n if (refused) return outreachRefusal(refused.status, refused.code, refused.message)\n for (const permission of extra) {\n if (!(await deps.holdsOrgCatalogPermission(decoded.uid, orgId, permission.key))) {\n return outreachRefusal(403, 'permission', permission.refusal)\n }\n }\n\n const org = (await deps.readOrg(orgId)) ?? {}\n const unentitled = outreachEntitlementRefusal(org)\n if (unentitled) return outreachRefusal(unentitled.status, unentitled.code, unentitled.message)\n const staff = decoded['staff'] === true\n const locked = await deps.lockdownRefusal({ request, staff, uid: decoded.uid, org })\n if (locked) return locked\n\n return {\n uid: decoded.uid,\n email: typeof decoded.email === 'string' ? decoded.email.trim().toLowerCase() : null,\n staff,\n orgId,\n org,\n isOrgAdmin: membership.
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/route-gate.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 { OUTREACH_MANAGE_ALL_PERMISSION } from '../constants/bundle-common'\nimport {\n isEmailVerified,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport type { DecodedIdToken } from 'firebase-admin/auth'\nimport { outreachEntitlementRefusal, outreachMembershipRefusal } from '../engine/outreach-access'\nimport { outreachRefusal, readOutreachDocumentId } from './route-http'\n\n/**\n * THE GATE EVERY SETTINGS, SEQUENCE AND ENROLLMENT ROUTE CLIMBS (AGL-2980).\n *\n * The console dispatcher has already refused a request while\n * `release_outreach` is off for it, an unverified account's token, a\n * lockdown with no org in scope, and a caller over the console rate limit —\n * every route here is registered under the `outreach/` prefix, which is what\n * puts it behind those. What the dispatcher cannot know is anything about\n * the member, so each rung below is this plugin's:\n *\n * 401 no bearer token, or one the verifier refused\n * 403 the account's address is unverified (an impersonation session is exempt)\n * 400 the request did not name an organization\n * 403 not a member of it, a site collaborator without org-wide reach, or\n * without `outreach.use` — or without a permission the route adds\n * 403 the organization does not hold the `outreach` entitlement\n * 423 lockdown, now with the organization and the account in scope\n *\n * The reach is org-wide for the reason the rules give: a sequence enrolls\n * people from every site of the organization, so none of it is a fact\n * about the one site a collaborator was invited to.\n *\n * `resolveOrgPermissions` is always given the concrete org id. Called\n * without one it resolves a caller with no org at all as that future org's\n * owner, which is right for a fresh signup and wrong for every question\n * asked here.\n */\n\nexport interface OutreachRouteGateDeps {\n verifyIdToken(idToken: string): Promise<DecodedIdToken>\n resolveOrgPermissions(\n uid: string,\n context: { orgId: string },\n ): Promise<{\n orgId: string | null\n /** `null` when the account is not on the organization's roster. */\n role: string | null\n isOwner: boolean\n orgWide: boolean\n permissions: Partial<Record<string, boolean | undefined>>\n }>\n /**\n * Whether the member holds a CATALOG permission (`data.manage`) as the\n * org's roles resolve it. Asked separately because the map\n * `resolveOrgPermissions` answers carries the legacy keys and the plugins'\n * own, never a catalog key: read there, a catalog key refuses everyone.\n */\n holdsOrgCatalogPermission(uid: string, orgId: string, key: string): Promise<boolean>\n readOrg(orgId: string): Promise<Record<string, unknown> | null>\n lockdownRefusal(options: {\n request: Request\n staff: boolean\n uid: string\n org: Record<string, unknown>\n }): Promise<Response | null>\n}\n\n/** Who is asking, once the gate let them through. */\nexport interface OutreachRouteCaller {\n uid: string\n /** The account's sign-in address, lower-cased, when the token carries one. */\n email: string | null\n staff: boolean\n orgId: string\n /** The organization document the entitlement was read from. */\n org: Record<string, unknown>\n /** An owner or admin of the organization, who may send from any member's mailbox. */\n isOrgAdmin: boolean\n}\n\n/**\n * A catalog permission a route asks for beyond `outreach.use`, with its\n * refusal — `data.manage` for a route that reads the CRM's people, which\n * the contacts' own rules gate on.\n */\nexport interface OutreachRouteExtraPermission {\n key: string\n /** Why the route needs it, as the refusal says it. */\n refusal: string\n}\n\nfunction bearer(request: Request): string | null {\n const authorization = request.headers.get('authorization') ?? ''\n return authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : null\n}\n\nexport async function outreachRouteGate(\n request: Request,\n rawOrgId: unknown,\n deps: OutreachRouteGateDeps,\n extra: readonly OutreachRouteExtraPermission[] = [],\n): Promise<Response | OutreachRouteCaller> {\n const token = bearer(request)\n if (!token) return outreachRefusal(401, 'unauthenticated', 'Sign in to use Sequences.')\n let decoded: DecodedIdToken\n try {\n decoded = await deps.verifyIdToken(token)\n } catch (error) {\n // A refused credential is the caller's; an Auth outage is ours and\n // propagates, so it answers 500 instead of \"sign in again\".\n if (invalidIdTokenResponse(error)) {\n return outreachRefusal(401, 'unauthenticated', 'Your sign-in could not be confirmed. Sign in again.')\n }\n throw error\n }\n if (!isEmailVerified(decoded) && !isImpersonationSession(decoded)) {\n return outreachRefusal(403, 'email-unverified', 'Verify your email address before using Sequences.')\n }\n\n const orgId = readOutreachDocumentId(rawOrgId)\n if (!orgId) return outreachRefusal(400, 'org-required', 'Open an organization before using Sequences.')\n\n const membership = await deps.resolveOrgPermissions(decoded.uid, { orgId })\n const refused = outreachMembershipRefusal(membership, orgId)\n if (refused) return outreachRefusal(refused.status, refused.code, refused.message)\n for (const permission of extra) {\n if (!(await deps.holdsOrgCatalogPermission(decoded.uid, orgId, permission.key))) {\n return outreachRefusal(403, 'permission', permission.refusal)\n }\n }\n\n const org = (await deps.readOrg(orgId)) ?? {}\n const unentitled = outreachEntitlementRefusal(org)\n if (unentitled) return outreachRefusal(unentitled.status, unentitled.code, unentitled.message)\n const staff = decoded['staff'] === true\n const locked = await deps.lockdownRefusal({ request, staff, uid: decoded.uid, org })\n if (locked) return locked\n\n return {\n uid: decoded.uid,\n email: typeof decoded.email === 'string' ? decoded.email.trim().toLowerCase() : null,\n staff,\n orgId,\n org,\n // \"Manage everyone's Sequences\" (AGL-4047), whose default is the owner\n // and admin `isOwner` named.\n isOrgAdmin: membership.permissions[OUTREACH_MANAGE_ALL_PERMISSION] === true,\n }\n}\n"],"names":["OUTREACH_MANAGE_ALL_PERMISSION","isEmailVerified","isImpersonationSession","invalidIdTokenResponse","outreachEntitlementRefusal","outreachMembershipRefusal","outreachRefusal","readOutreachDocumentId","bearer","request","authorization","headers","get","startsWith","slice","length","outreachRouteGate","rawOrgId","deps","extra","token","decoded","verifyIdToken","error","orgId","membership","resolveOrgPermissions","uid","refused","status","code","message","permission","holdsOrgCatalogPermission","key","refusal","org","readOrg","unentitled","staff","locked","lockdownRefusal","email","trim","toLowerCase","isOrgAdmin","permissions"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,8BAA8B,QAAQ,gCAA4B;AAC3E,SACEC,eAAe,EACfC,sBAAsB,QACjB,iDAAgD;AACvD,SAASC,sBAAsB,QAAQ,mDAAkD;AAEzF,SAASC,0BAA0B,EAAEC,yBAAyB,QAAQ,+BAA2B;AACjG,SAASC,eAAe,EAAEC,sBAAsB,QAAQ,kBAAc;AAmFtE,SAASC,OAAOC,OAAgB;QACRA;IAAtB,MAAMC,iBAAgBD,uBAAAA,QAAQE,OAAO,CAACC,GAAG,CAAC,4BAApBH,uBAAwC;IAC9D,OAAOC,cAAcG,UAAU,CAAC,aAAaH,cAAcI,KAAK,CAAC,UAAUC,MAAM,IAAI;AACvF;AAEA,OAAO,eAAeC,kBACpBP,OAAgB,EAChBQ,QAAiB,EACjBC,IAA2B,EAC3BC,QAAiD,EAAE;QA+BtC;IA7Bb,MAAMC,QAAQZ,OAAOC;IACrB,IAAI,CAACW,OAAO,OAAOd,gBAAgB,KAAK,mBAAmB;IAC3D,IAAIe;IACJ,IAAI;QACFA,UAAU,MAAMH,KAAKI,aAAa,CAACF;IACrC,EAAE,OAAOG,OAAO;QACd,mEAAmE;QACnE,4DAA4D;QAC5D,IAAIpB,uBAAuBoB,QAAQ;YACjC,OAAOjB,gBAAgB,KAAK,mBAAmB;QACjD;QACA,MAAMiB;IACR;IACA,IAAI,CAACtB,gBAAgBoB,YAAY,CAACnB,uBAAuBmB,UAAU;QACjE,OAAOf,gBAAgB,KAAK,oBAAoB;IAClD;IAEA,MAAMkB,QAAQjB,uBAAuBU;IACrC,IAAI,CAACO,OAAO,OAAOlB,gBAAgB,KAAK,gBAAgB;IAExD,MAAMmB,aAAa,MAAMP,KAAKQ,qBAAqB,CAACL,QAAQM,GAAG,EAAE;QAAEH;IAAM;IACzE,MAAMI,UAAUvB,0BAA0BoB,YAAYD;IACtD,IAAII,SAAS,OAAOtB,gBAAgBsB,QAAQC,MAAM,EAAED,QAAQE,IAAI,EAAEF,QAAQG,OAAO;IACjF,KAAK,MAAMC,cAAcb,MAAO;QAC9B,IAAI,CAAE,MAAMD,KAAKe,yBAAyB,CAACZ,QAAQM,GAAG,EAAEH,OAAOQ,WAAWE,GAAG,GAAI;YAC/E,OAAO5B,gBAAgB,KAAK,cAAc0B,WAAWG,OAAO;QAC9D;IACF;IAEA,MAAMC,OAAO,OAAA,MAAMlB,KAAKmB,OAAO,CAACb,kBAAnB,OAA8B,CAAC;IAC5C,MAAMc,aAAalC,2BAA2BgC;IAC9C,IAAIE,YAAY,OAAOhC,gBAAgBgC,WAAWT,MAAM,EAAES,WAAWR,IAAI,EAAEQ,WAAWP,OAAO;IAC7F,MAAMQ,QAAQlB,OAAO,CAAC,QAAQ,KAAK;IACnC,MAAMmB,SAAS,MAAMtB,KAAKuB,eAAe,CAAC;QAAEhC;QAAS8B;QAAOZ,KAAKN,QAAQM,GAAG;QAAES;IAAI;IAClF,IAAII,QAAQ,OAAOA;IAEnB,OAAO;QACLb,KAAKN,QAAQM,GAAG;QAChBe,OAAO,OAAOrB,QAAQqB,KAAK,KAAK,WAAWrB,QAAQqB,KAAK,CAACC,IAAI,GAAGC,WAAW,KAAK;QAChFL;QACAf;QACAY;QACA,uEAAuE;QACvE,6BAA6B;QAC7BS,YAAYpB,WAAWqB,WAAW,CAAC9C,+BAA+B,KAAK;IACzE;AACF"}
|
|
@@ -35,6 +35,8 @@ export interface OutreachSyncReport {
|
|
|
35
35
|
deliveries: number;
|
|
36
36
|
postponed: number;
|
|
37
37
|
paused: number;
|
|
38
|
+
/** Messages filed as a possible reply for a person to read (AGL-4088). */
|
|
39
|
+
possibleReplies: number;
|
|
38
40
|
}
|
|
39
41
|
/** Runs the sync job once — see the module note. */
|
|
40
42
|
export declare function runOutreachSyncJob(deps: OutreachRuntimeDeps, context: {
|
|
@@ -22,8 +22,9 @@ import { outreachEmailDomain } from "../engine/do-not-contact-domain.js";
|
|
|
22
22
|
import { outreachStoppedEntry } from "../engine/enrollment-activity.js";
|
|
23
23
|
import { isOutreachGatewayBlock, outreachGatewayBlockDetail } from "../engine/gateway-block.js";
|
|
24
24
|
import { mergeOutreachInboundMessages, outreachInboundMessagesOf } from "../engine/manual-reply.js";
|
|
25
|
+
import { matchOutreachPossibleReply, outreachPossibleReplyNote, outreachPossibleReplyTitle } from "../engine/possible-reply.js";
|
|
25
26
|
import { effectiveOutreachWindow, postponeOutreachForAutoReply } from "../engine/schedule.js";
|
|
26
|
-
import { decideOutreachThread } from "../engine/thread-classification.js";
|
|
27
|
+
import { decideOutreachThread, outreachAutoReplyEvidence } from "../engine/thread-classification.js";
|
|
27
28
|
import { outreachHeader, outreachThreadMessageFromGmail } from "../engine/thread-message.js";
|
|
28
29
|
import { mailboxRef } from "../mailboxes/mailbox-credentials.js";
|
|
29
30
|
import { OUTREACH_COLLECTIONS } from "../model/outreach.types.js";
|
|
@@ -83,6 +84,12 @@ import { outreachUnsubscribeMailbox } from "./unsubscribe-link.js";
|
|
|
83
84
|
* unsubscribe address; and messages from the people this mailbox is
|
|
84
85
|
* writing to, in a thread of their own.
|
|
85
86
|
*
|
|
87
|
+
* Then what is left of the new mail is read for POSSIBLE replies (AGL-4088,
|
|
88
|
+
* `engine/possible-reply`): a person writing to another of the
|
|
89
|
+
* organization's mailboxes, or a colleague at their company writing. Those
|
|
90
|
+
* stop nothing; each files the task "Possible reply from …" for a person to
|
|
91
|
+
* read.
|
|
92
|
+
*
|
|
86
93
|
* Every write it makes is idempotent — the do-not-contact list keeps its
|
|
87
94
|
* first entry, a topic opt-out keeps its first moment, a timeline entry is
|
|
88
95
|
* filed once per message — and the messages it handled are marked AFTER
|
|
@@ -99,6 +106,15 @@ import { outreachUnsubscribeMailbox } from "./unsubscribe-link.js";
|
|
|
99
106
|
/** How far a quiet mailbox's window may lag before its document is moved anyway. */ const MAILBOX_SYNC_WINDOW_REFRESH_MS = 60 * 60000;
|
|
100
107
|
const ENROLLMENT_HANDLED_MAX = 100;
|
|
101
108
|
/** Addresses in one `from:` search. */ const FROM_CHUNK = 15;
|
|
109
|
+
/** Messages one mailbox's run reads for a possible reply (AGL-4088). */ const POSSIBLE_REPLY_READS = 60;
|
|
110
|
+
/** The headers a possible reply is judged by: who, what, and whether a machine sent it. */ const POSSIBLE_REPLY_HEADERS = [
|
|
111
|
+
'From',
|
|
112
|
+
'Subject',
|
|
113
|
+
'Auto-Submitted',
|
|
114
|
+
'X-Autoreply',
|
|
115
|
+
'X-Autorespond',
|
|
116
|
+
'Precedence'
|
|
117
|
+
];
|
|
102
118
|
/** Runs the sync job once — see the module note. */ export async function runOutreachSyncJob(deps, context) {
|
|
103
119
|
const firestore = deps.firestore();
|
|
104
120
|
const report = {
|
|
@@ -110,7 +126,8 @@ const ENROLLMENT_HANDLED_MAX = 100;
|
|
|
110
126
|
gatewayBlocks: 0,
|
|
111
127
|
deliveries: 0,
|
|
112
128
|
postponed: 0,
|
|
113
|
-
paused: 0
|
|
129
|
+
paused: 0,
|
|
130
|
+
possibleReplies: 0
|
|
114
131
|
};
|
|
115
132
|
const all = await firestore.collectionGroup(OUTREACH_COLLECTIONS.mailboxes).get();
|
|
116
133
|
const byOrg = new Map();
|
|
@@ -130,6 +147,10 @@ const ENROLLMENT_HANDLED_MAX = 100;
|
|
|
130
147
|
const orgSnapshot = await firestore.collection('orgs').doc(orgId).get();
|
|
131
148
|
const org = orgSnapshot.exists ? orgSnapshot.data() : null;
|
|
132
149
|
if (!org || await deps.orgRefusal(orgId, org)) continue;
|
|
150
|
+
// Every enrollment the organization is still watching, read once per
|
|
151
|
+
// run and only when a mailbox has mail left to match (AGL-4088).
|
|
152
|
+
let orgWatched = null;
|
|
153
|
+
const watchedInOrg = ()=>orgWatched != null ? orgWatched : orgWatched = readWatchedEnrollments(firestore, orgId, context.nowMs);
|
|
133
154
|
for (const mailbox of mailboxes){
|
|
134
155
|
if (deps.now() >= context.deadlineMs) break;
|
|
135
156
|
try {
|
|
@@ -137,7 +158,8 @@ const ENROLLMENT_HANDLED_MAX = 100;
|
|
|
137
158
|
orgId,
|
|
138
159
|
mailbox,
|
|
139
160
|
report,
|
|
140
|
-
nowMs: context.nowMs
|
|
161
|
+
nowMs: context.nowMs,
|
|
162
|
+
watchedInOrg
|
|
141
163
|
});
|
|
142
164
|
report.mailboxes += 1;
|
|
143
165
|
} catch (error) {
|
|
@@ -318,6 +340,64 @@ async function syncMailbox(deps, firestore, input) {
|
|
|
318
340
|
markHandled(stub.id);
|
|
319
341
|
}
|
|
320
342
|
}
|
|
343
|
+
// 4b. What is left of the new mail, read for a possible reply (AGL-4088):
|
|
344
|
+
// the person writing to another of our mailboxes, or a colleague.
|
|
345
|
+
const leftover = inbound.filter(fresh).slice(0, POSSIBLE_REPLY_READS);
|
|
346
|
+
if (leftover.length) {
|
|
347
|
+
const enrollments = await input.watchedInOrg();
|
|
348
|
+
for (const stub of leftover){
|
|
349
|
+
if (!fresh(stub)) continue;
|
|
350
|
+
const metadata = await context.client.getMessage(stub.id, {
|
|
351
|
+
metadataHeaders: POSSIBLE_REPLY_HEADERS
|
|
352
|
+
});
|
|
353
|
+
const headers = Object.fromEntries(metadata.headers.map((entry)=>[
|
|
354
|
+
entry.name,
|
|
355
|
+
entry.value
|
|
356
|
+
]));
|
|
357
|
+
const header = (name)=>outreachHeader({
|
|
358
|
+
headers
|
|
359
|
+
}, name);
|
|
360
|
+
const fromHeader = header('From');
|
|
361
|
+
const sender = normalizeContactEmail(emailAddressOf(fromHeader));
|
|
362
|
+
const subject = header('Subject');
|
|
363
|
+
markHandled(stub.id);
|
|
364
|
+
if (!sender || context.selfAddresses.includes(sender)) continue;
|
|
365
|
+
if (outreachAutoReplyEvidence({
|
|
366
|
+
headers,
|
|
367
|
+
subject
|
|
368
|
+
})) continue;
|
|
369
|
+
const match = matchOutreachPossibleReply(sender, {
|
|
370
|
+
mailboxId: mailbox.id,
|
|
371
|
+
enrollments
|
|
372
|
+
});
|
|
373
|
+
if (!match) continue;
|
|
374
|
+
const enrolled = match.enrollment;
|
|
375
|
+
try {
|
|
376
|
+
await fileOutreachTask(deps, {
|
|
377
|
+
orgId,
|
|
378
|
+
hostId: enrolled.hostId,
|
|
379
|
+
link: outreachEnrollmentLink(enrolled),
|
|
380
|
+
dedupeKey: `possible-reply:${stub.id}`,
|
|
381
|
+
title: outreachPossibleReplyTitle(fromHeader.replace(/<[^>]*>/, '').replace(/"/g, '').trim(), sender),
|
|
382
|
+
notes: outreachPossibleReplyNote({
|
|
383
|
+
reason: match.reason,
|
|
384
|
+
enrolledName: enrolled.contactName || enrolled.email,
|
|
385
|
+
receivedBy: mailbox.sendAs || mailbox.email,
|
|
386
|
+
excerpt: [
|
|
387
|
+
subject && `Subject: ${subject}`,
|
|
388
|
+
metadata.snippet
|
|
389
|
+
].filter(Boolean).join('\n')
|
|
390
|
+
}),
|
|
391
|
+
kind: 'email',
|
|
392
|
+
dueAtMs: nowMs,
|
|
393
|
+
assigneeUid: mailbox.connectedByUid
|
|
394
|
+
});
|
|
395
|
+
report.possibleReplies += 1;
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.error('[outreach] the possible-reply task could not be filed', error);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
321
401
|
// 5. What went through: a day of silence after a send is a delivery
|
|
322
402
|
// (AGL-3326), judged on the enrollments as this run left them.
|
|
323
403
|
try {
|
|
@@ -356,6 +436,10 @@ async function syncMailbox(deps, firestore, input) {
|
|
|
356
436
|
});
|
|
357
437
|
}
|
|
358
438
|
}
|
|
439
|
+
/** Every enrollment of an organization written to within the watch window. */ async function readWatchedEnrollments(firestore, orgId, nowMs) {
|
|
440
|
+
const snapshot = await outreachOrgCollection(firestore, orgId, 'enrollments').where('lastSentAtMs', '>=', nowMs - OUTREACH_SYNC_WATCH_MS).get();
|
|
441
|
+
return snapshot.docs.map((doc)=>readStoredOutreachEnrollment(doc.id, doc.data())).filter((enrollment)=>enrollment !== null);
|
|
442
|
+
}
|
|
359
443
|
/** The sequence an enrollment runs through, read once per run. */ async function sequenceOf(context, sequenceId) {
|
|
360
444
|
var _context_sequences_get;
|
|
361
445
|
if (!context.sequences.has(sequenceId)) {
|
|
@@ -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 { emailAddressOf, emailExcerpt } from '@aglyn/aglyn/app-utils/email-text'\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 { mergeOutreachInboundMessages, outreachInboundMessagesOf } from '../engine/manual-reply'\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 { OutreachMailClient, OutreachMailSearch, OutreachMailStub } from '../transport/mail-client'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { creditOutreachReply } from './campaign-credit'\nimport { applyOutreachEvent, recordOutreachOptOut } from './enrollment-events'\nimport { creditOutreachGatewayDeliveries, recordOutreachGatewayBlock } from './gateway-ledger'\nimport { outreachMailboxSendingDomain } from '../storage/domain-intel-store'\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 { stampRecordEmailReply } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\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 * - every message the person wrote — a reply, an automatic answer, an\n * opt-out — is kept on the enrollment, its sender, time, threading ids\n * and trimmed text, for the console to show and answer (AGL-4020);\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 * and counts the block on the gateway ledger (AGL-3326), under the\n * gateway the bounce's `Remote-MTA` names.\n *\n * And once the mail is read, every email step of a watched enrollment that\n * is a day old with no bounce against it is credited to the ledger as\n * delivered (`./gateway-ledger`): the silence that says a gateway let the\n * sender through.\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 the provider rather than read in\n * full (`OutreachMailSearch`, AGL-3489):\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\n\n/** How far a quiet mailbox's window may lag before its document is moved anyway. */\nconst MAILBOX_SYNC_WINDOW_REFRESH_MS = 60 * 60_000\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 /** Email steps a day old with no bounce, credited to the gateway ledger (AGL-3326). */\n deliveries: 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: OutreachMailClient\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 deliveries: 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(client: OutreachMailClient, criteria: OutreachMailSearch): Promise<OutreachMailStub[]> {\n const found: OutreachMailStub[] = []\n let pageToken: string | null = null\n for (let page = 0; page < SEARCH_PAGES; page += 1) {\n const list = await client.searchMessages(criteria, { maxResults: 100, pageToken })\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 const watchedEnrollments: OutreachEnrollment[] = []\n for (const doc of watched.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment) continue\n watchedEnrollments.push(enrollment)\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 // 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: OutreachMailStub) => !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, { afterMs: sinceMs, notFromSelf: true })\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, { afterMs: sinceMs, from: ['mailer-daemon', '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, { afterMs: sinceMs, 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, { afterMs: sinceMs, from: chunk })) {\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 // 5. What went through: a day of silence after a send is a delivery\n // (AGL-3326), judged on the enrollments as this run left them.\n try {\n report.deliveries += await creditOutreachGatewayDeliveries(firestore, {\n orgId,\n enrollments: watchedEnrollments,\n sendingDomain: outreachMailboxSendingDomain(mailbox),\n resolveMx: deps.resolveMx,\n nowMs,\n })\n } catch (error) {\n console.error('[outreach] the gateway ledger could not credit deliveries', error)\n }\n\n const paused = await applyOutreachMailboxHealth(deps, { orgId, mailboxId: mailbox.id, delta: context.delta })\n if (paused) report.paused += 1\n // The window is moved only when something was handled or an hour has\n // passed: a quiet mailbox re-reading an hour's window is a few list calls,\n // where rewriting every mailbox every run was a write per mailbox every\n // fifteen minutes with nothing in it (AGL-3932). The handled ids keep a\n // re-read from acting twice.\n if (newlyHandled.length || nowMs - (mailbox.sync?.throughMs ?? 0) >= MAILBOX_SYNC_WINDOW_REFRESH_MS) {\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\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: emailExcerpt(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 // What they wrote, kept on the enrollment so the console can show it and\n // answer it in the thread (AGL-4020) — every message of theirs among\n // these, so a thread read whole also keeps a reply synced before they\n // were kept.\n const inbound = outreachInboundMessagesOf(messages, {\n selfAddresses: context.selfAddresses,\n unsubscribeAddress: context.unsubscribeAddress,\n threadSubject: enrollment.threadSubject,\n })\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: emailExcerpt(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): the record\n // system hears of the reply, and moves a lead nobody had worked to\n // Working (AGL-3080). Never throws.\n if (enrollment.target === 'lead' && enrollment.leadId && enrollment.email) {\n await stampRecordEmailReply({\n orgId: context.orgId,\n hostId: enrollment.hostId,\n emails: [enrollment.email],\n })\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 // The ledger learns the refusal (AGL-3326), whichever domain it was\n // at: a public provider refusing on policy is still that provider's\n // verdict on the sender, even though its domain is never listed.\n if (gateway) {\n await recordOutreachGatewayBlock(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n remoteMta: decidedBy?.bounce?.remoteMta ?? null,\n sendingDomain: outreachMailboxSendingDomain(context.mailbox),\n diagnostic,\n resolveMx: deps.resolveMx,\n nowMs,\n })\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 if (inbound.length) extra['inboundMessages'] = mergeOutreachInboundMessages(current.inboundMessages, inbound)\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","emailAddressOf","emailExcerpt","outreachMessageIdHeader","readOutreachDeliveryReport","outreachEmailDomain","outreachStoppedEntry","isOutreachGatewayBlock","outreachGatewayBlockDetail","mergeOutreachInboundMessages","outreachInboundMessagesOf","effectiveOutreachWindow","postponeOutreachForAutoReply","decideOutreachThread","outreachHeader","outreachThreadMessageFromGmail","mailboxRef","OUTREACH_COLLECTIONS","addOutreachDoNotContact","addOutreachDoNotContactDomain","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","creditOutreachReply","applyOutreachEvent","recordOutreachOptOut","creditOutreachGatewayDeliveries","recordOutreachGatewayBlock","outreachMailboxSendingDomain","applyOutreachMailboxHealth","emptyOutreachHealthDelta","noteOutreachMailboxReconnectRequired","stampRecordEmailReply","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","MAILBOX_SYNC_WINDOW_REFRESH_MS","ENROLLMENT_HANDLED_MAX","FROM_CHUNK","runOutreachSyncJob","deps","context","firestore","report","mailboxes","threads","replies","optOuts","bounces","gatewayBlocks","deliveries","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","criteria","found","pageToken","page","list","searchMessages","maxResults","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","watchedEnrollments","enrollment","known","threadId","gmailThreadIds","lastSentAtMs","sinceMs","Math","max","Number","sync","throughMs","handled","Set","handledMessageIds","newlyHandled","markHandled","has","add","fresh","stub","inbound","afterMs","notFromSelf","thread","getFullThread","applyMessages","from","getFullMessage","delivery","kind","failedAddresses","to","metadata","getMessage","metadataHeaders","headers","find","header","name","toLowerCase","value","includes","source","hostIds","hostId","detail","addresses","keys","start","length","chunk","slice","enrollments","sendingDomain","resolveMx","update","sequenceOf","sequenceId","snapshot","undefined","messageIdOf","scrubbedOutreachDiagnostic","diagnostic","pattern","RegExp","replace","fileInbound","messageId","link","direction","subject","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","emails","type","suppressBouncedEmail","enrollmentId","domain","gateway","remoteMta","bouncedAtMs","stampRecordEmailState","sequence","nextDueAtMs","receivedAtMs","timeZone","timezone","window","settings","random","extra","current","inboundMessages","waiting","changed","entry","sequenceName","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 { emailAddressOf, emailExcerpt } from '@aglyn/aglyn/app-utils/email-text'\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 { mergeOutreachInboundMessages, outreachInboundMessagesOf } from '../engine/manual-reply'\nimport {\n matchOutreachPossibleReply,\n outreachPossibleReplyNote,\n outreachPossibleReplyTitle,\n} from '../engine/possible-reply'\nimport { effectiveOutreachWindow, postponeOutreachForAutoReply } from '../engine/schedule'\nimport {\n decideOutreachThread,\n outreachAutoReplyEvidence,\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 { OutreachMailClient, OutreachMailSearch, OutreachMailStub } from '../transport/mail-client'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { creditOutreachReply } from './campaign-credit'\nimport { applyOutreachEvent, recordOutreachOptOut } from './enrollment-events'\nimport { creditOutreachGatewayDeliveries, recordOutreachGatewayBlock } from './gateway-ledger'\nimport { outreachMailboxSendingDomain } from '../storage/domain-intel-store'\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 { stampRecordEmailReply } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\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 * - every message the person wrote — a reply, an automatic answer, an\n * opt-out — is kept on the enrollment, its sender, time, threading ids\n * and trimmed text, for the console to show and answer (AGL-4020);\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 * and counts the block on the gateway ledger (AGL-3326), under the\n * gateway the bounce's `Remote-MTA` names.\n *\n * And once the mail is read, every email step of a watched enrollment that\n * is a day old with no bounce against it is credited to the ledger as\n * delivered (`./gateway-ledger`): the silence that says a gateway let the\n * sender through.\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 the provider rather than read in\n * full (`OutreachMailSearch`, AGL-3489):\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 * Then what is left of the new mail is read for POSSIBLE replies (AGL-4088,\n * `engine/possible-reply`): a person writing to another of the\n * organization's mailboxes, or a colleague at their company writing. Those\n * stop nothing; each files the task \"Possible reply from …\" for a person to\n * read.\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\n\n/** How far a quiet mailbox's window may lag before its document is moved anyway. */\nconst MAILBOX_SYNC_WINDOW_REFRESH_MS = 60 * 60_000\nconst ENROLLMENT_HANDLED_MAX = 100\n/** Addresses in one `from:` search. */\nconst FROM_CHUNK = 15\n/** Messages one mailbox's run reads for a possible reply (AGL-4088). */\nconst POSSIBLE_REPLY_READS = 60\n/** The headers a possible reply is judged by: who, what, and whether a machine sent it. */\nconst POSSIBLE_REPLY_HEADERS = ['From', 'Subject', 'Auto-Submitted', 'X-Autoreply', 'X-Autorespond', 'Precedence']\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 /** Email steps a day old with no bounce, credited to the gateway ledger (AGL-3326). */\n deliveries: number\n postponed: number\n paused: number\n /** Messages filed as a possible reply for a person to read (AGL-4088). */\n possibleReplies: number\n}\n\ninterface SyncContext {\n deps: OutreachRuntimeDeps\n firestore: Firestore\n orgId: string\n mailbox: OutreachMailbox\n client: OutreachMailClient\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 deliveries: 0,\n postponed: 0,\n paused: 0,\n possibleReplies: 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 // Every enrollment the organization is still watching, read once per\n // run and only when a mailbox has mail left to match (AGL-4088).\n let orgWatched: Promise<OutreachEnrollment[]> | null = null\n const watchedInOrg = () => (orgWatched ??= readWatchedEnrollments(firestore, orgId, context.nowMs))\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, watchedInOrg })\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(client: OutreachMailClient, criteria: OutreachMailSearch): Promise<OutreachMailStub[]> {\n const found: OutreachMailStub[] = []\n let pageToken: string | null = null\n for (let page = 0; page < SEARCH_PAGES; page += 1) {\n const list = await client.searchMessages(criteria, { maxResults: 100, pageToken })\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: {\n orgId: string\n mailbox: OutreachMailbox\n report: OutreachSyncReport\n nowMs: number\n watchedInOrg: () => Promise<OutreachEnrollment[]>\n },\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 const watchedEnrollments: OutreachEnrollment[] = []\n for (const doc of watched.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment) continue\n watchedEnrollments.push(enrollment)\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 // 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: OutreachMailStub) => !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, { afterMs: sinceMs, notFromSelf: true })\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, { afterMs: sinceMs, from: ['mailer-daemon', '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, { afterMs: sinceMs, 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, { afterMs: sinceMs, from: chunk })) {\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 // 4b. What is left of the new mail, read for a possible reply (AGL-4088):\n // the person writing to another of our mailboxes, or a colleague.\n const leftover = inbound.filter(fresh).slice(0, POSSIBLE_REPLY_READS)\n if (leftover.length) {\n const enrollments = await input.watchedInOrg()\n for (const stub of leftover) {\n if (!fresh(stub)) continue\n const metadata = await context.client.getMessage(stub.id, { metadataHeaders: POSSIBLE_REPLY_HEADERS })\n const headers = Object.fromEntries(metadata.headers.map((entry) => [entry.name, entry.value]))\n const header = (name: string) => outreachHeader({ headers }, name)\n const fromHeader = header('From')\n const sender = normalizeContactEmail(emailAddressOf(fromHeader))\n const subject = header('Subject')\n markHandled(stub.id)\n if (!sender || context.selfAddresses.includes(sender)) continue\n if (outreachAutoReplyEvidence({ headers, subject })) continue\n const match = matchOutreachPossibleReply(sender, { mailboxId: mailbox.id, enrollments })\n if (!match) continue\n const enrolled = match.enrollment\n try {\n await fileOutreachTask(deps, {\n orgId,\n hostId: enrolled.hostId,\n link: outreachEnrollmentLink(enrolled),\n dedupeKey: `possible-reply:${stub.id}`,\n title: outreachPossibleReplyTitle(fromHeader.replace(/<[^>]*>/, '').replace(/\"/g, '').trim(), sender),\n notes: outreachPossibleReplyNote({\n reason: match.reason,\n enrolledName: enrolled.contactName || enrolled.email,\n receivedBy: mailbox.sendAs || mailbox.email,\n excerpt: [subject && `Subject: ${subject}`, metadata.snippet].filter(Boolean).join('\\n'),\n }),\n kind: 'email',\n dueAtMs: nowMs,\n assigneeUid: mailbox.connectedByUid,\n })\n report.possibleReplies += 1\n } catch (error) {\n console.error('[outreach] the possible-reply task could not be filed', error)\n }\n }\n }\n\n // 5. What went through: a day of silence after a send is a delivery\n // (AGL-3326), judged on the enrollments as this run left them.\n try {\n report.deliveries += await creditOutreachGatewayDeliveries(firestore, {\n orgId,\n enrollments: watchedEnrollments,\n sendingDomain: outreachMailboxSendingDomain(mailbox),\n resolveMx: deps.resolveMx,\n nowMs,\n })\n } catch (error) {\n console.error('[outreach] the gateway ledger could not credit deliveries', error)\n }\n\n const paused = await applyOutreachMailboxHealth(deps, { orgId, mailboxId: mailbox.id, delta: context.delta })\n if (paused) report.paused += 1\n // The window is moved only when something was handled or an hour has\n // passed: a quiet mailbox re-reading an hour's window is a few list calls,\n // where rewriting every mailbox every run was a write per mailbox every\n // fifteen minutes with nothing in it (AGL-3932). The handled ids keep a\n // re-read from acting twice.\n if (newlyHandled.length || nowMs - (mailbox.sync?.throughMs ?? 0) >= MAILBOX_SYNC_WINDOW_REFRESH_MS) {\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\n/** Every enrollment of an organization written to within the watch window. */\nasync function readWatchedEnrollments(firestore: Firestore, orgId: string, nowMs: number): Promise<OutreachEnrollment[]> {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('lastSentAtMs', '>=', nowMs - OUTREACH_SYNC_WATCH_MS)\n .get()\n return snapshot.docs\n .map((doc) => readStoredOutreachEnrollment(doc.id, doc.data()))\n .filter((enrollment): enrollment is OutreachEnrollment => enrollment !== null)\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: emailExcerpt(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 // What they wrote, kept on the enrollment so the console can show it and\n // answer it in the thread (AGL-4020) — every message of theirs among\n // these, so a thread read whole also keeps a reply synced before they\n // were kept.\n const inbound = outreachInboundMessagesOf(messages, {\n selfAddresses: context.selfAddresses,\n unsubscribeAddress: context.unsubscribeAddress,\n threadSubject: enrollment.threadSubject,\n })\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: emailExcerpt(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): the record\n // system hears of the reply, and moves a lead nobody had worked to\n // Working (AGL-3080). Never throws.\n if (enrollment.target === 'lead' && enrollment.leadId && enrollment.email) {\n await stampRecordEmailReply({\n orgId: context.orgId,\n hostId: enrollment.hostId,\n emails: [enrollment.email],\n })\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 // The ledger learns the refusal (AGL-3326), whichever domain it was\n // at: a public provider refusing on policy is still that provider's\n // verdict on the sender, even though its domain is never listed.\n if (gateway) {\n await recordOutreachGatewayBlock(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n remoteMta: decidedBy?.bounce?.remoteMta ?? null,\n sendingDomain: outreachMailboxSendingDomain(context.mailbox),\n diagnostic,\n resolveMx: deps.resolveMx,\n nowMs,\n })\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 if (inbound.length) extra['inboundMessages'] = mergeOutreachInboundMessages(current.inboundMessages, inbound)\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","emailAddressOf","emailExcerpt","outreachMessageIdHeader","readOutreachDeliveryReport","outreachEmailDomain","outreachStoppedEntry","isOutreachGatewayBlock","outreachGatewayBlockDetail","mergeOutreachInboundMessages","outreachInboundMessagesOf","matchOutreachPossibleReply","outreachPossibleReplyNote","outreachPossibleReplyTitle","effectiveOutreachWindow","postponeOutreachForAutoReply","decideOutreachThread","outreachAutoReplyEvidence","outreachHeader","outreachThreadMessageFromGmail","mailboxRef","OUTREACH_COLLECTIONS","addOutreachDoNotContact","addOutreachDoNotContactDomain","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","creditOutreachReply","applyOutreachEvent","recordOutreachOptOut","creditOutreachGatewayDeliveries","recordOutreachGatewayBlock","outreachMailboxSendingDomain","applyOutreachMailboxHealth","emptyOutreachHealthDelta","noteOutreachMailboxReconnectRequired","stampRecordEmailReply","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","MAILBOX_SYNC_WINDOW_REFRESH_MS","ENROLLMENT_HANDLED_MAX","FROM_CHUNK","POSSIBLE_REPLY_READS","POSSIBLE_REPLY_HEADERS","runOutreachSyncJob","deps","context","firestore","report","mailboxes","threads","replies","optOuts","bounces","gatewayBlocks","deliveries","postponed","paused","possibleReplies","all","collectionGroup","get","byOrg","Map","doc","docs","orgId","ref","parent","id","mailbox","data","status","set","now","deadlineMs","orgSnapshot","collection","org","exists","orgRefusal","orgWatched","watchedInOrg","readWatchedEnrollments","nowMs","syncMailbox","error","mailboxId","errorCode","code","console","search","client","criteria","found","pageToken","page","list","searchMessages","maxResults","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","watchedEnrollments","enrollment","known","threadId","gmailThreadIds","lastSentAtMs","sinceMs","Math","max","Number","sync","throughMs","handled","Set","handledMessageIds","newlyHandled","markHandled","has","add","fresh","stub","inbound","afterMs","notFromSelf","thread","getFullThread","applyMessages","from","getFullMessage","delivery","kind","failedAddresses","to","metadata","getMessage","metadataHeaders","headers","find","header","name","toLowerCase","value","includes","source","hostIds","hostId","detail","addresses","keys","start","length","chunk","slice","leftover","enrollments","Object","fromEntries","entry","fromHeader","sender","subject","match","enrolled","link","dedupeKey","title","replace","trim","notes","enrolledName","contactName","receivedBy","excerpt","snippet","join","dueAtMs","assigneeUid","connectedByUid","sendingDomain","resolveMx","update","snapshot","sequenceOf","sequenceId","undefined","messageIdOf","scrubbedOutreachDiagnostic","diagnostic","pattern","RegExp","fileInbound","messageId","direction","inReplyTo","body","textBody","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","target","leadId","emails","type","suppressBouncedEmail","enrollmentId","domain","gateway","remoteMta","bouncedAtMs","stampRecordEmailState","sequence","nextDueAtMs","receivedAtMs","timeZone","timezone","window","settings","random","extra","current","inboundMessages","waiting","changed","sequenceName","assign"],"mappings":"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"}
|
|
@@ -32,7 +32,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
32
32
|
* objection at the dry run, before anything is written.
|
|
33
33
|
*=========================================*/ import { existingPackageItemsOf } from "@aglyn/aglyn/data-transfer";
|
|
34
34
|
import { checkEntitlement } from "@aglyn/aglyn/server";
|
|
35
|
-
import { OUTREACH_USE_PERMISSION } from "../constants/bundle-common.js";
|
|
35
|
+
import { OUTREACH_MANAGE_ALL_PERMISSION, OUTREACH_USE_PERMISSION } from "../constants/bundle-common.js";
|
|
36
36
|
import { defaultOutreachRouteDeps } from "../routes/register-routes.js";
|
|
37
37
|
import { removeOutreachSequence, saveOutreachSequence } from "../routes/sequence-routes.js";
|
|
38
38
|
import { readStoredOutreachMailbox, readStoredOutreachSequence } from "../storage/outreach-records.js";
|
|
@@ -55,7 +55,8 @@ import { OUTREACH_MAILBOX_REFERENCE_KIND, OUTREACH_SEQUENCES_TRANSFER_KEY, outre
|
|
|
55
55
|
staff: false,
|
|
56
56
|
orgId: ctx.orgId,
|
|
57
57
|
org,
|
|
58
|
-
|
|
58
|
+
// "Manage everyone's Sequences" (AGL-4047), as the routes ask it.
|
|
59
|
+
isOrgAdmin: membership.permissions[OUTREACH_MANAGE_ALL_PERMISSION] === true
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transfer/sequences-package.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * SEQUENCES IN A WORKSPACE PACKAGE — the server half (AGL-3535).\n *\n * Reads the workspace's sequences as package items and writes imported\n * ones through the editor's own save (`saveOutreachSequence`): the same\n * validator, the same placement rules (the site is the organization's,\n * the mailbox connected here and the importer's to send from, the\n * countries allowed), the same activity line — so an import can store\n * nothing the editor would refuse. A new sequence is a draft; a replaced\n * one keeps its status; undo deletes through the delete route's own rule\n * (a draft nobody was enrolled in) and restores through the save.\n *\n * The importer is resolved as the sequence routes resolve a caller: a\n * member of the organization, across all of it, holding Use Sequences, in\n * a workspace entitled to Sequences. Anything short of that is an\n * objection at the dry run, before anything is written.\n *=========================================*/\n\nimport { existingPackageItemsOf, type ExistingPackageItem } from '@aglyn/aglyn/data-transfer'\nimport type {\n TransferApplyResult,\n TransferApplyWriter,\n TransferPackageItemContent,\n TransferPackageItemWrite,\n TransferPackageRevertResult,\n TransferPackageRevertStep,\n TransferResourceContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport type { TransferReferenceKind, TransferRowResult } from '@aglyn/aglyn/data-transfer'\nimport { checkEntitlement } from '@aglyn/aglyn/server'\nimport { OUTREACH_USE_PERMISSION } from '../constants/bundle-common'\nimport { defaultOutreachRouteDeps } from '../routes/register-routes'\nimport type { OutreachRouteDeps } from '../routes/route-deps'\nimport type { OutreachRouteCaller } from '../routes/route-gate'\nimport { removeOutreachSequence, saveOutreachSequence } from '../routes/sequence-routes'\nimport { readStoredOutreachMailbox, readStoredOutreachSequence } from '../storage/outreach-records'\nimport { outreachOrgCollection } from '../storage/outreach-records'\nimport {\n OUTREACH_MAILBOX_REFERENCE_KIND,\n OUTREACH_SEQUENCES_TRANSFER_KEY,\n outreachSequencePackageContent,\n type OutreachSequencePackageContent,\n} from './sequences-package'\n\ntype Deps = Pick<OutreachRouteDeps, 'firestore' | 'now' | 'logOrgActivity' | 'gate'>\n\n/** The importer as a sequence route would admit them, or why it would not. */\nasync function importer(deps: Deps, ctx: TransferResourceContext): Promise<OutreachRouteCaller | string> {\n if (!ctx.actorUid) return 'Sequences are imported by a member of the organization.'\n const membership = await deps.gate.resolveOrgPermissions(ctx.actorUid, { orgId: ctx.orgId })\n if (!membership.role || membership.orgId !== ctx.orgId) return 'You are not a member of that organization.'\n if (!membership.orgWide) return 'Sequences covers the whole organization, and your access is to particular sites.'\n if (membership.permissions[OUTREACH_USE_PERMISSION] !== true) return 'Your role does not include Use Sequences.'\n const org = (await deps.gate.readOrg(ctx.orgId)) ?? {}\n if (!checkEntitlement(org, 'outreach')) return \"Sequences isn't available to this workspace yet.\"\n return { uid: ctx.actorUid, email: null, staff: false, orgId: ctx.orgId, org, isOrgAdmin: membership.isOwner }\n}\n\n/**\n * Sequences as package items — archived ones too, so one coming back under\n * its own id matches the archived sequence (which the save refuses to\n * change) rather than writing over it as new.\n */\nasync function readAll(deps: Deps, orgId: string, ids?: readonly string[]): Promise<Array<TransferPackageItemContent<OutreachSequencePackageContent>>> {\n const collection = outreachOrgCollection(deps.firestore(), orgId, 'sequences')\n const docs = ids\n ? (await Promise.all(ids.map((id) => collection.doc(id).get()))).filter((doc) => doc.exists)\n : (await collection.get()).docs\n const items: Array<TransferPackageItemContent<OutreachSequencePackageContent>> = []\n for (const doc of docs) {\n const stored = readStoredOutreachSequence(doc.id, doc.data())\n if (!stored) continue\n const content = outreachSequencePackageContent(stored)\n items.push({ kind: OUTREACH_SEQUENCES_TRANSFER_KEY, id: doc.id, name: content.name, content })\n }\n return items\n}\n\n/** The draft one item is saved as: its content, under a kept-both copy's new name. */\nfunction draftOf(write: TransferPackageItemWrite<OutreachSequencePackageContent>): OutreachSequencePackageContent {\n return { ...write.content, name: write.rename?.name ?? write.content.name }\n}\n\nasync function save(\n deps: Deps,\n caller: OutreachRouteCaller,\n write: TransferPackageItemWrite<OutreachSequencePackageContent>,\n judgeOnly: boolean,\n) {\n return saveOutreachSequence(deps, caller, {\n sequenceId: write.decision === 'replace' ? write.targetId : null,\n ...(write.decision === 'replace' ? {} : { newId: write.targetId }),\n draft: draftOf(write),\n judgeOnly,\n })\n}\n\nexport function createOutreachSequencesPackage(deps: Deps = defaultOutreachRouteDeps()) {\n return {\n async items(ctx: TransferResourceContext): Promise<ExistingPackageItem[]> {\n return existingPackageItemsOf(OUTREACH_SEQUENCES_TRANSFER_KEY, await readAll(deps, ctx.orgId))\n },\n\n async readItems(ctx: TransferResourceContext, ids: readonly string[]) {\n return readAll(deps, ctx.orgId, ids)\n },\n\n async problems(ctx: TransferResourceContext, write: TransferPackageItemWrite<OutreachSequencePackageContent>) {\n const caller = await importer(deps, ctx)\n if (typeof caller === 'string') return [caller]\n const judged = await save(deps, caller, write, true)\n if (judged.ok !== false) return []\n const errors = (judged.issues ?? []).filter((issue) => issue.severity === 'error').map((issue) => issue.message)\n return errors.length ? [...new Set(errors)] : [judged.message]\n },\n\n async referenceTargets(ctx: TransferResourceContext, kinds: readonly string[]): Promise<TransferReferenceKind[]> {\n if (!kinds.includes(OUTREACH_MAILBOX_REFERENCE_KIND)) return []\n const snapshot = await outreachOrgCollection(deps.firestore(), ctx.orgId, 'mailboxes').get()\n return [\n {\n kind: OUTREACH_MAILBOX_REFERENCE_KIND,\n label: 'Mailbox',\n targets: snapshot.docs.flatMap((doc) => {\n const mailbox = readStoredOutreachMailbox(doc.id, doc.data())\n return mailbox && mailbox.status !== 'disconnected' ? [{ id: doc.id, name: mailbox.email }] : []\n }),\n },\n ]\n },\n\n async writeItems(\n ctx: TransferResourceContext,\n items: ReadonlyArray<TransferPackageItemWrite<OutreachSequencePackageContent>>,\n writer: TransferApplyWriter,\n ): Promise<TransferApplyResult> {\n const caller = await importer(deps, ctx)\n const results: TransferRowResult[] = []\n for (const write of items) {\n const before = await writer.alreadyApplied(write.row)\n if (before) {\n results.push(before)\n continue\n }\n let result: TransferRowResult\n if (typeof caller === 'string') {\n result = { row: write.row, outcome: 'failed', message: caller }\n } else {\n const saved = await save(deps, caller, write, false)\n result =\n saved.ok === false\n ? { row: write.row, outcome: 'failed', recordId: write.targetId, message: saved.message }\n : { row: write.row, outcome: saved.created ? 'created' : 'updated', recordId: saved.sequence.id }\n }\n await writer.markApplied(result)\n results.push(result)\n }\n return { results, undo: [] }\n },\n\n async revertItems(\n ctx: TransferResourceContext,\n steps: ReadonlyArray<TransferPackageRevertStep<OutreachSequencePackageContent>>,\n ): Promise<TransferPackageRevertResult> {\n const caller = await importer(deps, ctx)\n const outcome: TransferPackageRevertResult = { done: [], refused: [] }\n for (const step of steps) {\n if (typeof caller === 'string') {\n outcome.refused.push({ id: step.id, reason: caller })\n continue\n }\n const done =\n step.action === 'delete'\n ? await removeOutreachSequence(deps, caller, step.id)\n : await saveOutreachSequence(deps, caller, { sequenceId: step.id, draft: outreachSequencePackageContent(step.content) })\n if (done.ok === false) outcome.refused.push({ id: step.id, reason: done.message })\n else outcome.done.push(step.id)\n }\n return outcome\n },\n }\n}\n"],"names":["existingPackageItemsOf","checkEntitlement","OUTREACH_USE_PERMISSION","defaultOutreachRouteDeps","removeOutreachSequence","saveOutreachSequence","readStoredOutreachMailbox","readStoredOutreachSequence","outreachOrgCollection","OUTREACH_MAILBOX_REFERENCE_KIND","OUTREACH_SEQUENCES_TRANSFER_KEY","outreachSequencePackageContent","importer","deps","ctx","actorUid","membership","gate","resolveOrgPermissions","orgId","role","orgWide","permissions","org","readOrg","uid","email","staff","isOrgAdmin","isOwner","readAll","ids","collection","firestore","docs","Promise","all","map","id","doc","get","filter","exists","items","stored","data","content","push","kind","name","draftOf","write","rename","save","caller","judgeOnly","sequenceId","decision","targetId","newId","draft","createOutreachSequencesPackage","readItems","problems","judged","ok","errors","issues","issue","severity","message","length","Set","referenceTargets","kinds","includes","snapshot","label","targets","flatMap","mailbox","status","writeItems","writer","results","before","alreadyApplied","row","result","outcome","saved","recordId","created","sequence","markApplied","undo","revertItems","steps","done","refused","step","reason","action"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transfer/sequences-package.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * SEQUENCES IN A WORKSPACE PACKAGE — the server half (AGL-3535).\n *\n * Reads the workspace's sequences as package items and writes imported\n * ones through the editor's own save (`saveOutreachSequence`): the same\n * validator, the same placement rules (the site is the organization's,\n * the mailbox connected here and the importer's to send from, the\n * countries allowed), the same activity line — so an import can store\n * nothing the editor would refuse. A new sequence is a draft; a replaced\n * one keeps its status; undo deletes through the delete route's own rule\n * (a draft nobody was enrolled in) and restores through the save.\n *\n * The importer is resolved as the sequence routes resolve a caller: a\n * member of the organization, across all of it, holding Use Sequences, in\n * a workspace entitled to Sequences. Anything short of that is an\n * objection at the dry run, before anything is written.\n *=========================================*/\n\nimport { existingPackageItemsOf, type ExistingPackageItem } from '@aglyn/aglyn/data-transfer'\nimport type {\n TransferApplyResult,\n TransferApplyWriter,\n TransferPackageItemContent,\n TransferPackageItemWrite,\n TransferPackageRevertResult,\n TransferPackageRevertStep,\n TransferResourceContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport type { TransferReferenceKind, TransferRowResult } from '@aglyn/aglyn/data-transfer'\nimport { checkEntitlement } from '@aglyn/aglyn/server'\nimport { OUTREACH_MANAGE_ALL_PERMISSION, OUTREACH_USE_PERMISSION } from '../constants/bundle-common'\nimport { defaultOutreachRouteDeps } from '../routes/register-routes'\nimport type { OutreachRouteDeps } from '../routes/route-deps'\nimport type { OutreachRouteCaller } from '../routes/route-gate'\nimport { removeOutreachSequence, saveOutreachSequence } from '../routes/sequence-routes'\nimport { readStoredOutreachMailbox, readStoredOutreachSequence } from '../storage/outreach-records'\nimport { outreachOrgCollection } from '../storage/outreach-records'\nimport {\n OUTREACH_MAILBOX_REFERENCE_KIND,\n OUTREACH_SEQUENCES_TRANSFER_KEY,\n outreachSequencePackageContent,\n type OutreachSequencePackageContent,\n} from './sequences-package'\n\ntype Deps = Pick<OutreachRouteDeps, 'firestore' | 'now' | 'logOrgActivity' | 'gate'>\n\n/** The importer as a sequence route would admit them, or why it would not. */\nasync function importer(deps: Deps, ctx: TransferResourceContext): Promise<OutreachRouteCaller | string> {\n if (!ctx.actorUid) return 'Sequences are imported by a member of the organization.'\n const membership = await deps.gate.resolveOrgPermissions(ctx.actorUid, { orgId: ctx.orgId })\n if (!membership.role || membership.orgId !== ctx.orgId) return 'You are not a member of that organization.'\n if (!membership.orgWide) return 'Sequences covers the whole organization, and your access is to particular sites.'\n if (membership.permissions[OUTREACH_USE_PERMISSION] !== true) return 'Your role does not include Use Sequences.'\n const org = (await deps.gate.readOrg(ctx.orgId)) ?? {}\n if (!checkEntitlement(org, 'outreach')) return \"Sequences isn't available to this workspace yet.\"\n return {\n uid: ctx.actorUid,\n email: null,\n staff: false,\n orgId: ctx.orgId,\n org,\n // \"Manage everyone's Sequences\" (AGL-4047), as the routes ask it.\n isOrgAdmin: membership.permissions[OUTREACH_MANAGE_ALL_PERMISSION] === true,\n }\n}\n\n/**\n * Sequences as package items — archived ones too, so one coming back under\n * its own id matches the archived sequence (which the save refuses to\n * change) rather than writing over it as new.\n */\nasync function readAll(deps: Deps, orgId: string, ids?: readonly string[]): Promise<Array<TransferPackageItemContent<OutreachSequencePackageContent>>> {\n const collection = outreachOrgCollection(deps.firestore(), orgId, 'sequences')\n const docs = ids\n ? (await Promise.all(ids.map((id) => collection.doc(id).get()))).filter((doc) => doc.exists)\n : (await collection.get()).docs\n const items: Array<TransferPackageItemContent<OutreachSequencePackageContent>> = []\n for (const doc of docs) {\n const stored = readStoredOutreachSequence(doc.id, doc.data())\n if (!stored) continue\n const content = outreachSequencePackageContent(stored)\n items.push({ kind: OUTREACH_SEQUENCES_TRANSFER_KEY, id: doc.id, name: content.name, content })\n }\n return items\n}\n\n/** The draft one item is saved as: its content, under a kept-both copy's new name. */\nfunction draftOf(write: TransferPackageItemWrite<OutreachSequencePackageContent>): OutreachSequencePackageContent {\n return { ...write.content, name: write.rename?.name ?? write.content.name }\n}\n\nasync function save(\n deps: Deps,\n caller: OutreachRouteCaller,\n write: TransferPackageItemWrite<OutreachSequencePackageContent>,\n judgeOnly: boolean,\n) {\n return saveOutreachSequence(deps, caller, {\n sequenceId: write.decision === 'replace' ? write.targetId : null,\n ...(write.decision === 'replace' ? {} : { newId: write.targetId }),\n draft: draftOf(write),\n judgeOnly,\n })\n}\n\nexport function createOutreachSequencesPackage(deps: Deps = defaultOutreachRouteDeps()) {\n return {\n async items(ctx: TransferResourceContext): Promise<ExistingPackageItem[]> {\n return existingPackageItemsOf(OUTREACH_SEQUENCES_TRANSFER_KEY, await readAll(deps, ctx.orgId))\n },\n\n async readItems(ctx: TransferResourceContext, ids: readonly string[]) {\n return readAll(deps, ctx.orgId, ids)\n },\n\n async problems(ctx: TransferResourceContext, write: TransferPackageItemWrite<OutreachSequencePackageContent>) {\n const caller = await importer(deps, ctx)\n if (typeof caller === 'string') return [caller]\n const judged = await save(deps, caller, write, true)\n if (judged.ok !== false) return []\n const errors = (judged.issues ?? []).filter((issue) => issue.severity === 'error').map((issue) => issue.message)\n return errors.length ? [...new Set(errors)] : [judged.message]\n },\n\n async referenceTargets(ctx: TransferResourceContext, kinds: readonly string[]): Promise<TransferReferenceKind[]> {\n if (!kinds.includes(OUTREACH_MAILBOX_REFERENCE_KIND)) return []\n const snapshot = await outreachOrgCollection(deps.firestore(), ctx.orgId, 'mailboxes').get()\n return [\n {\n kind: OUTREACH_MAILBOX_REFERENCE_KIND,\n label: 'Mailbox',\n targets: snapshot.docs.flatMap((doc) => {\n const mailbox = readStoredOutreachMailbox(doc.id, doc.data())\n return mailbox && mailbox.status !== 'disconnected' ? [{ id: doc.id, name: mailbox.email }] : []\n }),\n },\n ]\n },\n\n async writeItems(\n ctx: TransferResourceContext,\n items: ReadonlyArray<TransferPackageItemWrite<OutreachSequencePackageContent>>,\n writer: TransferApplyWriter,\n ): Promise<TransferApplyResult> {\n const caller = await importer(deps, ctx)\n const results: TransferRowResult[] = []\n for (const write of items) {\n const before = await writer.alreadyApplied(write.row)\n if (before) {\n results.push(before)\n continue\n }\n let result: TransferRowResult\n if (typeof caller === 'string') {\n result = { row: write.row, outcome: 'failed', message: caller }\n } else {\n const saved = await save(deps, caller, write, false)\n result =\n saved.ok === false\n ? { row: write.row, outcome: 'failed', recordId: write.targetId, message: saved.message }\n : { row: write.row, outcome: saved.created ? 'created' : 'updated', recordId: saved.sequence.id }\n }\n await writer.markApplied(result)\n results.push(result)\n }\n return { results, undo: [] }\n },\n\n async revertItems(\n ctx: TransferResourceContext,\n steps: ReadonlyArray<TransferPackageRevertStep<OutreachSequencePackageContent>>,\n ): Promise<TransferPackageRevertResult> {\n const caller = await importer(deps, ctx)\n const outcome: TransferPackageRevertResult = { done: [], refused: [] }\n for (const step of steps) {\n if (typeof caller === 'string') {\n outcome.refused.push({ id: step.id, reason: caller })\n continue\n }\n const done =\n step.action === 'delete'\n ? await removeOutreachSequence(deps, caller, step.id)\n : await saveOutreachSequence(deps, caller, { sequenceId: step.id, draft: outreachSequencePackageContent(step.content) })\n if (done.ok === false) outcome.refused.push({ id: step.id, reason: done.message })\n else outcome.done.push(step.id)\n }\n return outcome\n },\n }\n}\n"],"names":["existingPackageItemsOf","checkEntitlement","OUTREACH_MANAGE_ALL_PERMISSION","OUTREACH_USE_PERMISSION","defaultOutreachRouteDeps","removeOutreachSequence","saveOutreachSequence","readStoredOutreachMailbox","readStoredOutreachSequence","outreachOrgCollection","OUTREACH_MAILBOX_REFERENCE_KIND","OUTREACH_SEQUENCES_TRANSFER_KEY","outreachSequencePackageContent","importer","deps","ctx","actorUid","membership","gate","resolveOrgPermissions","orgId","role","orgWide","permissions","org","readOrg","uid","email","staff","isOrgAdmin","readAll","ids","collection","firestore","docs","Promise","all","map","id","doc","get","filter","exists","items","stored","data","content","push","kind","name","draftOf","write","rename","save","caller","judgeOnly","sequenceId","decision","targetId","newId","draft","createOutreachSequencesPackage","readItems","problems","judged","ok","errors","issues","issue","severity","message","length","Set","referenceTargets","kinds","includes","snapshot","label","targets","flatMap","mailbox","status","writeItems","writer","results","before","alreadyApplied","row","result","outcome","saved","recordId","created","sequence","markApplied","undo","revertItems","steps","done","refused","step","reason","action"],"mappings":"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"}
|