@aglyn/plugins-outreach 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +93 -0
- package/package.json +52 -0
- package/src/index.d.ts +22 -0
- package/src/index.js +29 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/compliance-section.d.ts +20 -0
- package/src/lib/components/compliance-section.js +291 -0
- package/src/lib/components/compliance-section.js.map +1 -0
- package/src/lib/components/enroll-dialog.d.ts +36 -0
- package/src/lib/components/enroll-dialog.js +656 -0
- package/src/lib/components/enroll-dialog.js.map +1 -0
- package/src/lib/components/enrollments-table.d.ts +28 -0
- package/src/lib/components/enrollments-table.js +375 -0
- package/src/lib/components/enrollments-table.js.map +1 -0
- package/src/lib/components/mailbox-card.d.ts +46 -0
- package/src/lib/components/mailbox-card.js +517 -0
- package/src/lib/components/mailbox-card.js.map +1 -0
- package/src/lib/components/mailboxes-section.d.ts +24 -0
- package/src/lib/components/mailboxes-section.js +258 -0
- package/src/lib/components/mailboxes-section.js.map +1 -0
- package/src/lib/components/outreach-console-page.d.ts +17 -0
- package/src/lib/components/outreach-console-page.js +81 -0
- package/src/lib/components/outreach-console-page.js.map +1 -0
- package/src/lib/components/outreach-console-sections.d.ts +43 -0
- package/src/lib/components/outreach-console-sections.js +55 -0
- package/src/lib/components/outreach-console-sections.js.map +1 -0
- package/src/lib/components/outreach-ui.d.ts +51 -0
- package/src/lib/components/outreach-ui.js +171 -0
- package/src/lib/components/outreach-ui.js.map +1 -0
- package/src/lib/components/send-window-fields.d.ts +25 -0
- package/src/lib/components/send-window-fields.js +178 -0
- package/src/lib/components/send-window-fields.js.map +1 -0
- package/src/lib/components/sequence-detail.d.ts +54 -0
- package/src/lib/components/sequence-detail.js +435 -0
- package/src/lib/components/sequence-detail.js.map +1 -0
- package/src/lib/components/sequence-editor.d.ts +37 -0
- package/src/lib/components/sequence-editor.js +485 -0
- package/src/lib/components/sequence-editor.js.map +1 -0
- package/src/lib/components/sequence-mailbox-picker.d.ts +44 -0
- package/src/lib/components/sequence-mailbox-picker.js +137 -0
- package/src/lib/components/sequence-mailbox-picker.js.map +1 -0
- package/src/lib/components/sequence-preview.d.ts +27 -0
- package/src/lib/components/sequence-preview.js +146 -0
- package/src/lib/components/sequence-preview.js.map +1 -0
- package/src/lib/components/sequence-step-card.d.ts +31 -0
- package/src/lib/components/sequence-step-card.js +326 -0
- package/src/lib/components/sequence-step-card.js.map +1 -0
- package/src/lib/components/sequences-section.d.ts +40 -0
- package/src/lib/components/sequences-section.js +310 -0
- package/src/lib/components/sequences-section.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +38 -0
- package/src/lib/components/use-outreach-api.js +139 -0
- package/src/lib/components/use-outreach-api.js.map +1 -0
- package/src/lib/components/use-outreach-crm.d.ts +50 -0
- package/src/lib/components/use-outreach-crm.js +200 -0
- package/src/lib/components/use-outreach-crm.js.map +1 -0
- package/src/lib/components/use-outreach-data.d.ts +55 -0
- package/src/lib/components/use-outreach-data.js +199 -0
- package/src/lib/components/use-outreach-data.js.map +1 -0
- package/src/lib/components/use-outreach-mailbox-api.d.ts +30 -0
- package/src/lib/components/use-outreach-mailbox-api.js +101 -0
- package/src/lib/components/use-outreach-mailbox-api.js.map +1 -0
- package/src/lib/components/use-outreach-mailboxes.d.ts +22 -0
- package/src/lib/components/use-outreach-mailboxes.js +68 -0
- package/src/lib/components/use-outreach-mailboxes.js.map +1 -0
- package/src/lib/components/use-outreach-settings.d.ts +17 -0
- package/src/lib/components/use-outreach-settings.js +64 -0
- package/src/lib/components/use-outreach-settings.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +68 -0
- package/src/lib/constants/api-routes.js +57 -0
- package/src/lib/constants/api-routes.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +46 -0
- package/src/lib/constants/bundle-common.js +55 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/constants/runtime-jobs.d.ts +24 -0
- package/src/lib/constants/runtime-jobs.js +24 -0
- package/src/lib/constants/runtime-jobs.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +38 -0
- package/src/lib/declarations.console-server.js +99 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/engine/compose.d.ts +86 -0
- package/src/lib/engine/compose.js +212 -0
- package/src/lib/engine/compose.js.map +1 -0
- package/src/lib/engine/delivery-status.d.ts +53 -0
- package/src/lib/engine/delivery-status.js +184 -0
- package/src/lib/engine/delivery-status.js.map +1 -0
- package/src/lib/engine/do-not-contact.d.ts +18 -0
- package/src/lib/engine/do-not-contact.js +34 -0
- package/src/lib/engine/do-not-contact.js.map +1 -0
- package/src/lib/engine/enrollment-state.d.ts +156 -0
- package/src/lib/engine/enrollment-state.js +303 -0
- package/src/lib/engine/enrollment-state.js.map +1 -0
- package/src/lib/engine/gates.d.ts +125 -0
- package/src/lib/engine/gates.js +232 -0
- package/src/lib/engine/gates.js.map +1 -0
- package/src/lib/engine/index.d.ts +28 -0
- package/src/lib/engine/index.js +33 -0
- package/src/lib/engine/index.js.map +1 -0
- package/src/lib/engine/mailbox-health.d.ts +49 -0
- package/src/lib/engine/mailbox-health.js +83 -0
- package/src/lib/engine/mailbox-health.js.map +1 -0
- package/src/lib/engine/opt-out-intent.d.ts +41 -0
- package/src/lib/engine/opt-out-intent.js +220 -0
- package/src/lib/engine/opt-out-intent.js.map +1 -0
- package/src/lib/engine/recipient-country.d.ts +41 -0
- package/src/lib/engine/recipient-country.js +171 -0
- package/src/lib/engine/recipient-country.js.map +1 -0
- package/src/lib/engine/schedule.d.ts +70 -0
- package/src/lib/engine/schedule.js +115 -0
- package/src/lib/engine/schedule.js.map +1 -0
- package/src/lib/engine/sending-capacity.d.ts +96 -0
- package/src/lib/engine/sending-capacity.js +172 -0
- package/src/lib/engine/sending-capacity.js.map +1 -0
- package/src/lib/engine/sequence-validation.d.ts +90 -0
- package/src/lib/engine/sequence-validation.js +291 -0
- package/src/lib/engine/sequence-validation.js.map +1 -0
- package/src/lib/engine/thread-classification.d.ts +78 -0
- package/src/lib/engine/thread-classification.js +224 -0
- package/src/lib/engine/thread-classification.js.map +1 -0
- package/src/lib/engine/thread-message.d.ts +80 -0
- package/src/lib/engine/thread-message.js +143 -0
- package/src/lib/engine/thread-message.js.map +1 -0
- package/src/lib/enrollment/enroll-people.d.ts +81 -0
- package/src/lib/enrollment/enroll-people.js +250 -0
- package/src/lib/enrollment/enroll-people.js.map +1 -0
- package/src/lib/enrollment/gate-lookups.d.ts +32 -0
- package/src/lib/enrollment/gate-lookups.js +154 -0
- package/src/lib/enrollment/gate-lookups.js.map +1 -0
- package/src/lib/mailboxes/mailbox-api.d.ts +126 -0
- package/src/lib/mailboxes/mailbox-api.js +66 -0
- package/src/lib/mailboxes/mailbox-api.js.map +1 -0
- package/src/lib/mailboxes/mailbox-credentials.d.ts +95 -0
- package/src/lib/mailboxes/mailbox-credentials.js +102 -0
- package/src/lib/mailboxes/mailbox-credentials.js.map +1 -0
- package/src/lib/mailboxes/mailbox-erasure.d.ts +69 -0
- package/src/lib/mailboxes/mailbox-erasure.js +148 -0
- package/src/lib/mailboxes/mailbox-erasure.js.map +1 -0
- package/src/lib/mailboxes/mailbox-gate.d.ts +75 -0
- package/src/lib/mailboxes/mailbox-gate.js +98 -0
- package/src/lib/mailboxes/mailbox-gate.js.map +1 -0
- package/src/lib/mailboxes/mailbox-revoke.d.ts +59 -0
- package/src/lib/mailboxes/mailbox-revoke.js +41 -0
- package/src/lib/mailboxes/mailbox-revoke.js.map +1 -0
- package/src/lib/mailboxes/mailbox-routes.d.ts +119 -0
- package/src/lib/mailboxes/mailbox-routes.js +605 -0
- package/src/lib/mailboxes/mailbox-routes.js.map +1 -0
- package/src/lib/mailboxes/mailbox-settings.d.ts +110 -0
- package/src/lib/mailboxes/mailbox-settings.js +212 -0
- package/src/lib/mailboxes/mailbox-settings.js.map +1 -0
- package/src/lib/mailboxes/mailbox-transport.d.ts +64 -0
- package/src/lib/mailboxes/mailbox-transport.js +94 -0
- package/src/lib/mailboxes/mailbox-transport.js.map +1 -0
- package/src/lib/mailboxes/oauth-redirect.d.ts +41 -0
- package/src/lib/mailboxes/oauth-redirect.js +63 -0
- package/src/lib/mailboxes/oauth-redirect.js.map +1 -0
- package/src/lib/mailboxes/oauth-state.d.ts +152 -0
- package/src/lib/mailboxes/oauth-state.js +212 -0
- package/src/lib/mailboxes/oauth-state.js.map +1 -0
- package/src/lib/mailboxes/outreach-config.d.ts +64 -0
- package/src/lib/mailboxes/outreach-config.js +77 -0
- package/src/lib/mailboxes/outreach-config.js.map +1 -0
- package/src/lib/mailboxes/register-mailbox-routes.d.ts +47 -0
- package/src/lib/mailboxes/register-mailbox-routes.js +135 -0
- package/src/lib/mailboxes/register-mailbox-routes.js.map +1 -0
- package/src/lib/model/compliance-settings.d.ts +78 -0
- package/src/lib/model/compliance-settings.js +155 -0
- package/src/lib/model/compliance-settings.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +210 -0
- package/src/lib/model/outreach-api.js +28 -0
- package/src/lib/model/outreach-api.js.map +1 -0
- package/src/lib/model/outreach.types.d.ts +563 -0
- package/src/lib/model/outreach.types.js +225 -0
- package/src/lib/model/outreach.types.js.map +1 -0
- package/src/lib/model/sequence-draft.d.ts +84 -0
- package/src/lib/model/sequence-draft.js +239 -0
- package/src/lib/model/sequence-draft.js.map +1 -0
- package/src/lib/model/step-preview.d.ts +45 -0
- package/src/lib/model/step-preview.js +98 -0
- package/src/lib/model/step-preview.js.map +1 -0
- package/src/lib/model/stored-records.d.ts +23 -0
- package/src/lib/model/stored-records.js +84 -0
- package/src/lib/model/stored-records.js.map +1 -0
- package/src/lib/plugin.d.ts +46 -0
- package/src/lib/plugin.js +86 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/routes/enroll-routes.d.ts +64 -0
- package/src/lib/routes/enroll-routes.js +350 -0
- package/src/lib/routes/enroll-routes.js.map +1 -0
- package/src/lib/routes/enrollment-routes.d.ts +19 -0
- package/src/lib/routes/enrollment-routes.js +171 -0
- package/src/lib/routes/enrollment-routes.js.map +1 -0
- package/src/lib/routes/platform-deps.d.ts +51 -0
- package/src/lib/routes/platform-deps.js +66 -0
- package/src/lib/routes/platform-deps.js.map +1 -0
- package/src/lib/routes/preview-routes.d.ts +33 -0
- package/src/lib/routes/preview-routes.js +117 -0
- package/src/lib/routes/preview-routes.js.map +1 -0
- package/src/lib/routes/register-routes.d.ts +22 -0
- package/src/lib/routes/register-routes.js +92 -0
- package/src/lib/routes/register-routes.js.map +1 -0
- package/src/lib/routes/route-deps.d.ts +49 -0
- package/src/lib/routes/route-deps.js +27 -0
- package/src/lib/routes/route-deps.js.map +1 -0
- package/src/lib/routes/route-gate.d.ts +94 -0
- package/src/lib/routes/route-gate.js +87 -0
- package/src/lib/routes/route-gate.js.map +1 -0
- package/src/lib/routes/route-http.d.ts +32 -0
- package/src/lib/routes/route-http.js +62 -0
- package/src/lib/routes/route-http.js.map +1 -0
- package/src/lib/routes/sequence-routes.d.ts +32 -0
- package/src/lib/routes/sequence-routes.js +359 -0
- package/src/lib/routes/sequence-routes.js.map +1 -0
- package/src/lib/routes/settings-routes.d.ts +30 -0
- package/src/lib/routes/settings-routes.js +73 -0
- package/src/lib/routes/settings-routes.js.map +1 -0
- package/src/lib/runtime/enrollment-events.d.ts +86 -0
- package/src/lib/runtime/enrollment-events.js +141 -0
- package/src/lib/runtime/enrollment-events.js.map +1 -0
- package/src/lib/runtime/fixtures/fake-gmail.d.ts +121 -0
- package/src/lib/runtime/fixtures/fake-gmail.js +312 -0
- package/src/lib/runtime/fixtures/fake-gmail.js.map +1 -0
- package/src/lib/runtime/mailbox-health-store.d.ts +47 -0
- package/src/lib/runtime/mailbox-health-store.js +168 -0
- package/src/lib/runtime/mailbox-health-store.js.map +1 -0
- package/src/lib/runtime/person-erasure.d.ts +39 -0
- package/src/lib/runtime/person-erasure.js +57 -0
- package/src/lib/runtime/person-erasure.js.map +1 -0
- package/src/lib/runtime/platform-runtime-deps.d.ts +29 -0
- package/src/lib/runtime/platform-runtime-deps.js +98 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -0
- package/src/lib/runtime/runtime-deps.d.ts +77 -0
- package/src/lib/runtime/runtime-deps.js +18 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -0
- package/src/lib/runtime/send-job.d.ts +42 -0
- package/src/lib/runtime/send-job.js +780 -0
- package/src/lib/runtime/send-job.js.map +1 -0
- package/src/lib/runtime/sync-job.d.ts +41 -0
- package/src/lib/runtime/sync-job.js +480 -0
- package/src/lib/runtime/sync-job.js.map +1 -0
- package/src/lib/runtime/timeline.d.ts +67 -0
- package/src/lib/runtime/timeline.js +93 -0
- package/src/lib/runtime/timeline.js.map +1 -0
- package/src/lib/runtime/unsubscribe-link.d.ts +75 -0
- package/src/lib/runtime/unsubscribe-link.js +121 -0
- package/src/lib/runtime/unsubscribe-link.js.map +1 -0
- package/src/lib/runtime/unsubscribe-route.d.ts +24 -0
- package/src/lib/runtime/unsubscribe-route.js +103 -0
- package/src/lib/runtime/unsubscribe-route.js.map +1 -0
- package/src/lib/server.d.ts +53 -0
- package/src/lib/server.js +94 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/storage/compliance-settings-store.d.ts +44 -0
- package/src/lib/storage/compliance-settings-store.js +78 -0
- package/src/lib/storage/compliance-settings-store.js.map +1 -0
- package/src/lib/storage/do-not-contact-store.d.ts +60 -0
- package/src/lib/storage/do-not-contact-store.js +168 -0
- package/src/lib/storage/do-not-contact-store.js.map +1 -0
- package/src/lib/storage/outreach-records.d.ts +33 -0
- package/src/lib/storage/outreach-records.js +35 -0
- package/src/lib/storage/outreach-records.js.map +1 -0
- package/src/lib/subprocessors.d.ts +48 -0
- package/src/lib/subprocessors.js +70 -0
- package/src/lib/subprocessors.js.map +1 -0
- package/src/lib/transport/gmail-client.d.ts +145 -0
- package/src/lib/transport/gmail-client.js +229 -0
- package/src/lib/transport/gmail-client.js.map +1 -0
- package/src/lib/transport/gmail-errors.d.ts +84 -0
- package/src/lib/transport/gmail-errors.js +138 -0
- package/src/lib/transport/gmail-errors.js.map +1 -0
- package/src/lib/transport/google-oauth.d.ts +138 -0
- package/src/lib/transport/google-oauth.js +247 -0
- package/src/lib/transport/google-oauth.js.map +1 -0
- package/src/lib/transport/http.d.ts +53 -0
- package/src/lib/transport/http.js +79 -0
- package/src/lib/transport/http.js.map +1 -0
- package/src/lib/transport/rfc5322.d.ts +116 -0
- package/src/lib/transport/rfc5322.js +330 -0
- package/src/lib/transport/rfc5322.js.map +1 -0
- package/src/lib/transport/send-message.d.ts +69 -0
- package/src/lib/transport/send-message.js +77 -0
- package/src/lib/transport/send-message.js.map +1 -0
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
*/ // Each registry from its own module, not the `@aglyn/aglyn/server` barrel:
|
|
17
|
+
// boot needs one registry, not the whole server surface.
|
|
18
|
+
import { registerPluginConsoleCron } from "@aglyn/aglyn/plugin-manager/plugin-console-crons";
|
|
19
|
+
import { listPluginOrgErasers, registerPluginOrgEraser } from "@aglyn/aglyn/plugin-manager/plugin-org-erasure";
|
|
20
|
+
import { registerPluginPersonEraser } from "@aglyn/aglyn/plugin-manager/plugin-person-erasure";
|
|
21
|
+
import { listPluginUserErasers, registerPluginUserEraser } from "@aglyn/aglyn/plugin-manager/plugin-user-erasure";
|
|
22
|
+
import { OUTREACH_PLUGIN_ID } from "./constants/bundle-common.js";
|
|
23
|
+
import { OUTREACH_SEND_JOB_ID, OUTREACH_SYNC_JOB_ID } from "./constants/runtime-jobs.js";
|
|
24
|
+
/**
|
|
25
|
+
* Outreach's CONSOLE-ONLY server declarations (AGL-2978), named under
|
|
26
|
+
* `consoleServerDeclarations` in `plugins.config.json` and run at the
|
|
27
|
+
* console's boot.
|
|
28
|
+
*
|
|
29
|
+
* The erasers are registered here rather than from `serverDeclarations`,
|
|
30
|
+
* which the tenant runtime also loads: the workspace eraser revokes each
|
|
31
|
+
* rep's Google grant, and the account eraser (AGL-3106) revokes and deletes
|
|
32
|
+
* the person's own, and both open a sealed token with `OUTREACH_TOKEN_KEY`,
|
|
33
|
+
* which only the console holds. The tenant may not so much as bundle the code
|
|
34
|
+
* that does (`outreach-credential-isolation.spec.ts`). Every erasure runs in
|
|
35
|
+
* the console, so this is where the erasers are needed.
|
|
36
|
+
*
|
|
37
|
+
* The sending runtime (AGL-2981) is declared here too, for the same reason:
|
|
38
|
+
* the send and sync jobs open each rep's sealed grant, so they run on the
|
|
39
|
+
* console's `plugin-console-crons` tick and nowhere else, and the person
|
|
40
|
+
* eraser removes the enrollments a workspace kept about someone it erases.
|
|
41
|
+
*
|
|
42
|
+
* Light at boot: every module that does the work is imported when a job, an
|
|
43
|
+
* erasure or a tick first asks for it, so the boot cost is the registration.
|
|
44
|
+
*/ export function registerOutreachConsoleServerDeclarations() {
|
|
45
|
+
// Idempotent against the REGISTRY, so a reset (a spec) registers again.
|
|
46
|
+
if (!listPluginOrgErasers().includes(OUTREACH_PLUGIN_ID)) {
|
|
47
|
+
registerPluginOrgEraser(async (request)=>{
|
|
48
|
+
const { createOutreachOrgEraser, defaultOutreachErasureDeps } = await import("./mailboxes/mailbox-erasure.js");
|
|
49
|
+
return createOutreachOrgEraser(defaultOutreachErasureDeps())(request);
|
|
50
|
+
}, {
|
|
51
|
+
pluginId: OUTREACH_PLUGIN_ID
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (!listPluginUserErasers().includes(OUTREACH_PLUGIN_ID)) {
|
|
55
|
+
registerPluginUserEraser(async (request)=>{
|
|
56
|
+
const { createOutreachUserEraser, defaultOutreachErasureDeps } = await import("./mailboxes/mailbox-erasure.js");
|
|
57
|
+
return createOutreachUserEraser(defaultOutreachErasureDeps())(request);
|
|
58
|
+
}, {
|
|
59
|
+
pluginId: OUTREACH_PLUGIN_ID
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// A person erased from a workspace takes their enrollments with them; the
|
|
63
|
+
// do-not-contact entry stays, stripped of anything that names them.
|
|
64
|
+
registerPluginPersonEraser(async (request)=>(await runtime()).platformOutreachPersonEraser()(request), {
|
|
65
|
+
pluginId: OUTREACH_PLUGIN_ID
|
|
66
|
+
});
|
|
67
|
+
// The sending runtime, on the console's fifteen-minute tick.
|
|
68
|
+
registerPluginConsoleCron({
|
|
69
|
+
id: OUTREACH_SEND_JOB_ID,
|
|
70
|
+
label: 'Sequence sends',
|
|
71
|
+
drives: 'Sends every sequence step that has come due, from the rep’s own connected mailbox, and files each on the contact’s timeline. If it stops, no sequence sends anything and every task step waits.',
|
|
72
|
+
run: async (context)=>{
|
|
73
|
+
const [{ runOutreachSendJob }, platform] = await Promise.all([
|
|
74
|
+
import("./runtime/send-job.js"),
|
|
75
|
+
runtime()
|
|
76
|
+
]);
|
|
77
|
+
return runOutreachSendJob(platform.platformOutreachRuntimeDeps(), context);
|
|
78
|
+
}
|
|
79
|
+
}, {
|
|
80
|
+
pluginId: OUTREACH_PLUGIN_ID
|
|
81
|
+
});
|
|
82
|
+
registerPluginConsoleCron({
|
|
83
|
+
id: OUTREACH_SYNC_JOB_ID,
|
|
84
|
+
label: 'Sequence replies and bounces',
|
|
85
|
+
drives: 'Reads each connected mailbox for replies, out-of-office answers, opt-outs and bounces, and stops or postpones the sequence each one is about. If it stops, a person who replied or asked to be left alone keeps getting follow-ups, and a mailbox that bounces never pauses itself.',
|
|
86
|
+
run: async (context)=>{
|
|
87
|
+
const [{ runOutreachSyncJob }, platform] = await Promise.all([
|
|
88
|
+
import("./runtime/sync-job.js"),
|
|
89
|
+
runtime()
|
|
90
|
+
]);
|
|
91
|
+
return runOutreachSyncJob(platform.platformOutreachRuntimeDeps(), context);
|
|
92
|
+
}
|
|
93
|
+
}, {
|
|
94
|
+
pluginId: OUTREACH_PLUGIN_ID
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** The runtime's platform reach, loaded when a job or an erasure first runs. */ const runtime = ()=>import("./runtime/platform-runtime-deps.js");
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=declarations.console-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/outreach/src/lib/declarations.console-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// Each registry from its own module, not the `@aglyn/aglyn/server` barrel:\n// boot needs one registry, not the whole server surface.\nimport { registerPluginConsoleCron } from '@aglyn/aglyn/plugin-manager/plugin-console-crons'\nimport {\n listPluginOrgErasers,\n registerPluginOrgEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-org-erasure'\nimport { registerPluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport {\n listPluginUserErasers,\n registerPluginUserEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-user-erasure'\nimport { OUTREACH_PLUGIN_ID } from './constants/bundle-common'\nimport { OUTREACH_SEND_JOB_ID, OUTREACH_SYNC_JOB_ID } from './constants/runtime-jobs'\n\n/**\n * Outreach's CONSOLE-ONLY server declarations (AGL-2978), named under\n * `consoleServerDeclarations` in `plugins.config.json` and run at the\n * console's boot.\n *\n * The erasers are registered here rather than from `serverDeclarations`,\n * which the tenant runtime also loads: the workspace eraser revokes each\n * rep's Google grant, and the account eraser (AGL-3106) revokes and deletes\n * the person's own, and both open a sealed token with `OUTREACH_TOKEN_KEY`,\n * which only the console holds. The tenant may not so much as bundle the code\n * that does (`outreach-credential-isolation.spec.ts`). Every erasure runs in\n * the console, so this is where the erasers are needed.\n *\n * The sending runtime (AGL-2981) is declared here too, for the same reason:\n * the send and sync jobs open each rep's sealed grant, so they run on the\n * console's `plugin-console-crons` tick and nowhere else, and the person\n * eraser removes the enrollments a workspace kept about someone it erases.\n *\n * Light at boot: every module that does the work is imported when a job, an\n * erasure or a tick first asks for it, so the boot cost is the registration.\n */\nexport function registerOutreachConsoleServerDeclarations(): void {\n // Idempotent against the REGISTRY, so a reset (a spec) registers again.\n if (!listPluginOrgErasers().includes(OUTREACH_PLUGIN_ID)) {\n registerPluginOrgEraser(\n async (request) => {\n const { createOutreachOrgEraser, defaultOutreachErasureDeps } = await import(\n './mailboxes/mailbox-erasure'\n )\n return createOutreachOrgEraser(defaultOutreachErasureDeps())(request)\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n }\n if (!listPluginUserErasers().includes(OUTREACH_PLUGIN_ID)) {\n registerPluginUserEraser(\n async (request) => {\n const { createOutreachUserEraser, defaultOutreachErasureDeps } = await import(\n './mailboxes/mailbox-erasure'\n )\n return createOutreachUserEraser(defaultOutreachErasureDeps())(request)\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n }\n // A person erased from a workspace takes their enrollments with them; the\n // do-not-contact entry stays, stripped of anything that names them.\n registerPluginPersonEraser(\n async (request) => (await runtime()).platformOutreachPersonEraser()(request),\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n // The sending runtime, on the console's fifteen-minute tick.\n registerPluginConsoleCron(\n {\n id: OUTREACH_SEND_JOB_ID,\n label: 'Sequence sends',\n drives:\n 'Sends every sequence step that has come due, from the rep’s own connected mailbox, and files each on the contact’s timeline. If it stops, no sequence sends anything and every task step waits.',\n run: async (context) => {\n const [{ runOutreachSendJob }, platform] = await Promise.all([import('./runtime/send-job'), runtime()])\n return runOutreachSendJob(platform.platformOutreachRuntimeDeps(), context)\n },\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n registerPluginConsoleCron(\n {\n id: OUTREACH_SYNC_JOB_ID,\n label: 'Sequence replies and bounces',\n drives:\n 'Reads each connected mailbox for replies, out-of-office answers, opt-outs and bounces, and stops or postpones the sequence each one is about. If it stops, a person who replied or asked to be left alone keeps getting follow-ups, and a mailbox that bounces never pauses itself.',\n run: async (context) => {\n const [{ runOutreachSyncJob }, platform] = await Promise.all([import('./runtime/sync-job'), runtime()])\n return runOutreachSyncJob(platform.platformOutreachRuntimeDeps(), context)\n },\n },\n { pluginId: OUTREACH_PLUGIN_ID },\n )\n}\n\n/** The runtime's platform reach, loaded when a job or an erasure first runs. */\nconst runtime = () => import('./runtime/platform-runtime-deps')\n"],"names":["registerPluginConsoleCron","listPluginOrgErasers","registerPluginOrgEraser","registerPluginPersonEraser","listPluginUserErasers","registerPluginUserEraser","OUTREACH_PLUGIN_ID","OUTREACH_SEND_JOB_ID","OUTREACH_SYNC_JOB_ID","registerOutreachConsoleServerDeclarations","includes","request","createOutreachOrgEraser","defaultOutreachErasureDeps","pluginId","createOutreachUserEraser","runtime","platformOutreachPersonEraser","id","label","drives","run","context","runOutreachSendJob","platform","Promise","all","platformOutreachRuntimeDeps","runOutreachSyncJob"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,2EAA2E;AAC3E,yDAAyD;AACzD,SAASA,yBAAyB,QAAQ,mDAAkD;AAC5F,SACEC,oBAAoB,EACpBC,uBAAuB,QAClB,iDAAgD;AACvD,SAASC,0BAA0B,QAAQ,oDAAmD;AAC9F,SACEC,qBAAqB,EACrBC,wBAAwB,QACnB,kDAAiD;AACxD,SAASC,kBAAkB,QAAQ,+BAA2B;AAC9D,SAASC,oBAAoB,EAAEC,oBAAoB,QAAQ,8BAA0B;AAErF;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASC;IACd,wEAAwE;IACxE,IAAI,CAACR,uBAAuBS,QAAQ,CAACJ,qBAAqB;QACxDJ,wBACE,OAAOS;YACL,MAAM,EAAEC,uBAAuB,EAAEC,0BAA0B,EAAE,GAAG,MAAM,MAAM,CAC1E;YAEF,OAAOD,wBAAwBC,8BAA8BF;QAC/D,GACA;YAAEG,UAAUR;QAAmB;IAEnC;IACA,IAAI,CAACF,wBAAwBM,QAAQ,CAACJ,qBAAqB;QACzDD,yBACE,OAAOM;YACL,MAAM,EAAEI,wBAAwB,EAAEF,0BAA0B,EAAE,GAAG,MAAM,MAAM,CAC3E;YAEF,OAAOE,yBAAyBF,8BAA8BF;QAChE,GACA;YAAEG,UAAUR;QAAmB;IAEnC;IACA,0EAA0E;IAC1E,oEAAoE;IACpEH,2BACE,OAAOQ,UAAY,AAAC,CAAA,MAAMK,SAAQ,EAAGC,4BAA4B,GAAGN,UACpE;QAAEG,UAAUR;IAAmB;IAEjC,6DAA6D;IAC7DN,0BACE;QACEkB,IAAIX;QACJY,OAAO;QACPC,QACE;QACFC,KAAK,OAAOC;YACV,MAAM,CAAC,EAAEC,kBAAkB,EAAE,EAAEC,SAAS,GAAG,MAAMC,QAAQC,GAAG,CAAC;gBAAC,MAAM,CAAC;gBAAuBV;aAAU;YACtG,OAAOO,mBAAmBC,SAASG,2BAA2B,IAAIL;QACpE;IACF,GACA;QAAER,UAAUR;IAAmB;IAEjCN,0BACE;QACEkB,IAAIV;QACJW,OAAO;QACPC,QACE;QACFC,KAAK,OAAOC;YACV,MAAM,CAAC,EAAEM,kBAAkB,EAAE,EAAEJ,SAAS,GAAG,MAAMC,QAAQC,GAAG,CAAC;gBAAC,MAAM,CAAC;gBAAuBV;aAAU;YACtG,OAAOY,mBAAmBJ,SAASG,2BAA2B,IAAIL;QACpE;IACF,GACA;QAAER,UAAUR;IAAmB;AAEnC;AAEA,8EAA8E,GAC9E,MAAMU,UAAU,IAAM,MAAM,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
import { type CrmMergeContext } from '@aglyn/aglyn/app-utils/crm-email-templates';
|
|
18
|
+
import type { OutreachEnrollment, OutreachOrgSettings, OutreachSequence } from '../model/outreach.types';
|
|
19
|
+
/** The way out every email offers, word for word. */
|
|
20
|
+
export declare const OUTREACH_OPT_OUT_LINE = "Not relevant? Reply \"no\" and I won't email again.";
|
|
21
|
+
/** The sentence that says what the email is. */
|
|
22
|
+
export declare function outreachSolicitationStatement(senderName: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* The message the transport sends. Header values are single lines already;
|
|
25
|
+
* the text uses `\n` line breaks, which the transport writes as CRLF.
|
|
26
|
+
*/
|
|
27
|
+
export interface ComposedOutreachEmail {
|
|
28
|
+
/** The recipient's address, normalized. */
|
|
29
|
+
to: string;
|
|
30
|
+
subject: string;
|
|
31
|
+
/** The plain-text body, footer included. */
|
|
32
|
+
text: string;
|
|
33
|
+
/** The provider thread a reply step is sent into. */
|
|
34
|
+
threadId?: string;
|
|
35
|
+
/** The `Message-ID` this email answers, angle brackets included. */
|
|
36
|
+
inReplyTo?: string;
|
|
37
|
+
/** Every `Message-ID` in the thread so far, oldest first, space-separated. */
|
|
38
|
+
references?: string;
|
|
39
|
+
/** The one-click unsubscribe URL, for `List-Unsubscribe` with `List-Unsubscribe-Post`. */
|
|
40
|
+
listUnsubscribeUrl?: string;
|
|
41
|
+
/** A `mailto:` URI for `List-Unsubscribe`. */
|
|
42
|
+
listUnsubscribeMailto?: string;
|
|
43
|
+
}
|
|
44
|
+
export type OutreachComposeErrorCode = 'missing_postal_address' | 'missing_legal_name' | 'not_an_email_step' | 'invalid_recipient' | 'missing_subject' | 'subject_reply_prefix' | 'missing_body' | 'missing_thread' | 'invalid_unsubscribe_url' | 'invalid_unsubscribe_mailto';
|
|
45
|
+
export interface OutreachComposeError {
|
|
46
|
+
code: OutreachComposeErrorCode;
|
|
47
|
+
message: string;
|
|
48
|
+
}
|
|
49
|
+
export interface ComposeOutreachEmailInput {
|
|
50
|
+
sequence: Pick<OutreachSequence, 'steps'>;
|
|
51
|
+
enrollment: Pick<OutreachEnrollment, 'email' | 'stepIndex' | 'personalLine' | 'threadSubject' | 'messageIds' | 'gmailThreadId'>;
|
|
52
|
+
orgSettings: Pick<OutreachOrgSettings, 'legalName' | 'brandName' | 'postalAddress'> | null | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* What the CRM resolver reads: the contact, the sending site's group id,
|
|
55
|
+
* the sender (the mailbox's display name and address), the site. Its
|
|
56
|
+
* `extra` may carry fields of the caller's own; the personal line is added
|
|
57
|
+
* here.
|
|
58
|
+
*/
|
|
59
|
+
merge: CrmMergeContext | null | undefined;
|
|
60
|
+
/** The CRM template's body, read by the caller, when the step names a template. */
|
|
61
|
+
templateBody?: string | null;
|
|
62
|
+
/** The signed one-click unsubscribe URL, minted by the runtime. */
|
|
63
|
+
listUnsubscribeUrl?: string | null;
|
|
64
|
+
/** An address, or a `mailto:` URI, that unsubscribes whoever writes to it. */
|
|
65
|
+
listUnsubscribeMailto?: string | null;
|
|
66
|
+
}
|
|
67
|
+
export interface OutreachComposeResult {
|
|
68
|
+
email: ComposedOutreachEmail | null;
|
|
69
|
+
error: OutreachComposeError | null;
|
|
70
|
+
/** Merge fields that rendered empty, unique, in order of appearance. */
|
|
71
|
+
unresolvedFields: string[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The footer every email ends with, or the reason it cannot be written.
|
|
75
|
+
*
|
|
76
|
+
* The postal address may be stored over several lines; the footer prints it
|
|
77
|
+
* on one, the parts separated by commas.
|
|
78
|
+
*/
|
|
79
|
+
export declare function composeOutreachFooter(settings: Pick<OutreachOrgSettings, 'legalName' | 'brandName' | 'postalAddress'> | null | undefined): {
|
|
80
|
+
footer: string | null;
|
|
81
|
+
error: OutreachComposeError | null;
|
|
82
|
+
};
|
|
83
|
+
/** A `Message-ID` in its header form — `<id@host>` — or `null` for one that is not. */
|
|
84
|
+
export declare function outreachMessageIdHeader(value: unknown): string | null;
|
|
85
|
+
/** The email one step sends to one person — see the module note. */
|
|
86
|
+
export declare function composeOutreachEmail(input: ComposeOutreachEmailInput): OutreachComposeResult;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /*==========================================
|
|
18
|
+
* WHAT EXACTLY IS SENT (AGL-2979).
|
|
19
|
+
*
|
|
20
|
+
* The composer turns a step, an enrollment and the organization's settings
|
|
21
|
+
* into the message the transport sends: the recipient, the subject, the
|
|
22
|
+
* plain text, and the headers that thread it and let the recipient leave.
|
|
23
|
+
* The transport adds the From line and the MIME; nothing here knows either.
|
|
24
|
+
*
|
|
25
|
+
* ## The footer is not optional
|
|
26
|
+
*
|
|
27
|
+
* Every email ends with the organization's legal name, its postal address,
|
|
28
|
+
* a sentence saying the email is a business solicitation, and the way out —
|
|
29
|
+
* reply "no". Those are what CAN-SPAM requires of a commercial email, and
|
|
30
|
+
* the composer appends them itself, after the merge, as literal text: no
|
|
31
|
+
* template can leave them off, and no merge field can rewrite them. With no
|
|
32
|
+
* postal address or no legal name, it refuses to compose at all.
|
|
33
|
+
*
|
|
34
|
+
* ## Threading
|
|
35
|
+
*
|
|
36
|
+
* An email after the first is a reply in the thread unless its step says
|
|
37
|
+
* otherwise: `Re:` and the thread's own subject, `In-Reply-To` the last
|
|
38
|
+
* message sent into the thread, `References` every one of them, and the
|
|
39
|
+
* provider thread to send into. A `Re:` on an email that answers nothing is
|
|
40
|
+
* a deception, so a reply step with no thread to answer is refused rather
|
|
41
|
+
* than sent with a borrowed one, and a subject that starts with `Re:` or
|
|
42
|
+
* `Fwd:` on an email that starts a thread is refused too.
|
|
43
|
+
*
|
|
44
|
+
* ## Merge fields
|
|
45
|
+
*
|
|
46
|
+
* The CRM's own resolver fills them, from the documents the runtime already
|
|
47
|
+
* read, and the enrollment's personal line joins them as
|
|
48
|
+
* `{{enrollment.personalLine}}` through the resolver's `extra` fields.
|
|
49
|
+
* Fields that render empty are returned beside the message, for the
|
|
50
|
+
* runtime to decide whether "Hi ," is worth sending.
|
|
51
|
+
*==========================================*/ import { normalizeContactEmail } from "@aglyn/aglyn/app-utils/contacts";
|
|
52
|
+
import { CRM_EMAIL_SUBJECT_MAX } from "@aglyn/aglyn/app-utils/crm";
|
|
53
|
+
import { resolveCrmMergeFields } from "@aglyn/aglyn/app-utils/crm-email-templates";
|
|
54
|
+
import { crmThreadSubject } from "@aglyn/aglyn/app-utils/crm-inbound";
|
|
55
|
+
import { normalizeOutreachPersonalLine } from "./gates.js";
|
|
56
|
+
import { isInThreadEmailStep, OUTREACH_PERSONAL_LINE_FIELD, outreachSubjectHasReplyPrefix } from "./sequence-validation.js";
|
|
57
|
+
/** The way out every email offers, word for word. */ export const OUTREACH_OPT_OUT_LINE = 'Not relevant? Reply "no" and I won\'t email again.';
|
|
58
|
+
/** The sentence that says what the email is. */ export function outreachSolicitationStatement(senderName) {
|
|
59
|
+
return `This is a business solicitation from ${senderName}.`;
|
|
60
|
+
}
|
|
61
|
+
const oneLine = (value)=>typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
62
|
+
/**
|
|
63
|
+
* The footer every email ends with, or the reason it cannot be written.
|
|
64
|
+
*
|
|
65
|
+
* The postal address may be stored over several lines; the footer prints it
|
|
66
|
+
* on one, the parts separated by commas.
|
|
67
|
+
*/ export function composeOutreachFooter(settings) {
|
|
68
|
+
var _ref;
|
|
69
|
+
const postalAddress = String((_ref = settings == null ? void 0 : settings.postalAddress) != null ? _ref : '').split(/\r\n|\r|\n/).map((line)=>oneLine(line).replace(/,+$/, '')).filter(Boolean).join(', ');
|
|
70
|
+
if (!postalAddress) {
|
|
71
|
+
return {
|
|
72
|
+
footer: null,
|
|
73
|
+
error: {
|
|
74
|
+
code: 'missing_postal_address',
|
|
75
|
+
message: "This email can't be sent without your organization's postal address. Add it in Sequences settings."
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const legalName = oneLine(settings == null ? void 0 : settings.legalName);
|
|
80
|
+
if (!legalName) {
|
|
81
|
+
return {
|
|
82
|
+
footer: null,
|
|
83
|
+
error: {
|
|
84
|
+
code: 'missing_legal_name',
|
|
85
|
+
message: "This email can't be sent without your organization's legal name. Add it in Sequences settings."
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const senderName = oneLine(settings == null ? void 0 : settings.brandName) || legalName;
|
|
90
|
+
return {
|
|
91
|
+
footer: `${legalName} · ${postalAddress}\n${outreachSolicitationStatement(senderName)} ${OUTREACH_OPT_OUT_LINE}`,
|
|
92
|
+
error: null
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** A `Message-ID` in its header form — `<id@host>` — or `null` for one that is not. */ export function outreachMessageIdHeader(value) {
|
|
96
|
+
const bare = String(value != null ? value : '').trim().replace(/^<+/, '').replace(/>+$/, '');
|
|
97
|
+
if (!bare || /[\s<>]/.test(bare)) return null;
|
|
98
|
+
return `<${bare}>`;
|
|
99
|
+
}
|
|
100
|
+
function unsubscribeUrl(value) {
|
|
101
|
+
const raw = String(value != null ? value : '').trim();
|
|
102
|
+
if (!raw) return undefined;
|
|
103
|
+
if (/[\r\n]/.test(raw)) return null;
|
|
104
|
+
try {
|
|
105
|
+
const url = new URL(raw);
|
|
106
|
+
return url.protocol === 'https:' ? url.href : null;
|
|
107
|
+
} catch (unused) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function unsubscribeMailto(value) {
|
|
112
|
+
const raw = String(value != null ? value : '').trim();
|
|
113
|
+
if (!raw) return undefined;
|
|
114
|
+
if (/[\r\n]/.test(raw)) return null;
|
|
115
|
+
if (!/^mailto:/i.test(raw)) {
|
|
116
|
+
const address = normalizeContactEmail(raw);
|
|
117
|
+
return address ? `mailto:${address}?subject=unsubscribe` : null;
|
|
118
|
+
}
|
|
119
|
+
let target = raw.slice('mailto:'.length).split('?')[0];
|
|
120
|
+
try {
|
|
121
|
+
target = decodeURIComponent(target);
|
|
122
|
+
} catch (unused) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return normalizeContactEmail(target) ? raw : null;
|
|
126
|
+
}
|
|
127
|
+
const refuse = (code, message)=>({
|
|
128
|
+
email: null,
|
|
129
|
+
error: {
|
|
130
|
+
code,
|
|
131
|
+
message
|
|
132
|
+
},
|
|
133
|
+
unresolvedFields: []
|
|
134
|
+
});
|
|
135
|
+
/** The email one step sends to one person — see the module note. */ export function composeOutreachEmail(input) {
|
|
136
|
+
var _ref, _input_merge, _ref1, _input_templateBody, _step_body;
|
|
137
|
+
var _input_sequence, _input_enrollment, _input_merge1;
|
|
138
|
+
const { footer, error } = composeOutreachFooter(input.orgSettings);
|
|
139
|
+
if (error) return {
|
|
140
|
+
email: null,
|
|
141
|
+
error,
|
|
142
|
+
unresolvedFields: []
|
|
143
|
+
};
|
|
144
|
+
const steps = (_ref = (_input_sequence = input.sequence) == null ? void 0 : _input_sequence.steps) != null ? _ref : [];
|
|
145
|
+
const stepIndex = (_input_enrollment = input.enrollment) == null ? void 0 : _input_enrollment.stepIndex;
|
|
146
|
+
const step = steps[stepIndex];
|
|
147
|
+
if ((step == null ? void 0 : step.kind) !== 'email') {
|
|
148
|
+
return refuse('not_an_email_step', 'The step this enrollment is on does not send an email.');
|
|
149
|
+
}
|
|
150
|
+
const to = normalizeContactEmail(input.enrollment.email);
|
|
151
|
+
if (!to) {
|
|
152
|
+
return refuse('invalid_recipient', "This enrollment's address isn't a valid email address.");
|
|
153
|
+
}
|
|
154
|
+
const context = _extends({}, (_input_merge = input.merge) != null ? _input_merge : {}, {
|
|
155
|
+
extra: _extends({}, (_ref1 = (_input_merge1 = input.merge) == null ? void 0 : _input_merge1.extra) != null ? _ref1 : {}, {
|
|
156
|
+
[OUTREACH_PERSONAL_LINE_FIELD]: normalizeOutreachPersonalLine(input.enrollment.personalLine)
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
const unresolved = [];
|
|
160
|
+
const render = (text)=>{
|
|
161
|
+
const result = resolveCrmMergeFields(text, context);
|
|
162
|
+
for (const key of result.unresolved)if (!unresolved.includes(key)) unresolved.push(key);
|
|
163
|
+
return result.text;
|
|
164
|
+
};
|
|
165
|
+
const email = {
|
|
166
|
+
to,
|
|
167
|
+
subject: '',
|
|
168
|
+
text: ''
|
|
169
|
+
};
|
|
170
|
+
if (isInThreadEmailStep(steps, stepIndex)) {
|
|
171
|
+
var _input_enrollment_messageIds, _input_enrollment_gmailThreadId;
|
|
172
|
+
const threadSubject = crmThreadSubject(input.enrollment.threadSubject);
|
|
173
|
+
const messageIds = ((_input_enrollment_messageIds = input.enrollment.messageIds) != null ? _input_enrollment_messageIds : []).map(outreachMessageIdHeader).filter((id)=>id !== null);
|
|
174
|
+
const threadId = String((_input_enrollment_gmailThreadId = input.enrollment.gmailThreadId) != null ? _input_enrollment_gmailThreadId : '').trim();
|
|
175
|
+
if (!threadSubject || !messageIds.length || !threadId) {
|
|
176
|
+
return refuse('missing_thread', 'This email replies in the thread, and no earlier email in the thread was sent.');
|
|
177
|
+
}
|
|
178
|
+
email.subject = `Re: ${threadSubject}`.slice(0, CRM_EMAIL_SUBJECT_MAX);
|
|
179
|
+
email.threadId = threadId;
|
|
180
|
+
email.inReplyTo = messageIds[messageIds.length - 1];
|
|
181
|
+
email.references = messageIds.join(' ');
|
|
182
|
+
} else {
|
|
183
|
+
var _step_subject;
|
|
184
|
+
const subject = oneLine(render(String((_step_subject = step.subject) != null ? _step_subject : '')));
|
|
185
|
+
if (!subject) return refuse('missing_subject', 'This email has no subject.');
|
|
186
|
+
if (outreachSubjectHasReplyPrefix(subject)) {
|
|
187
|
+
return refuse('subject_reply_prefix', 'This email starts a thread, so its subject may not begin with "Re:" or "Fwd:".');
|
|
188
|
+
}
|
|
189
|
+
email.subject = subject.slice(0, CRM_EMAIL_SUBJECT_MAX);
|
|
190
|
+
}
|
|
191
|
+
const source = step.templateId ? String((_input_templateBody = input.templateBody) != null ? _input_templateBody : '') : String((_step_body = step.body) != null ? _step_body : '');
|
|
192
|
+
const body = render(source).replace(/\r\n?/g, '\n').split('\n').map((line)=>line.replace(/[ \t]+$/, '')).join('\n').replace(/^\n+|\n+$/g, '');
|
|
193
|
+
if (!body.trim()) return refuse('missing_body', 'This email has no body.');
|
|
194
|
+
email.text = `${body}\n\n${footer}`;
|
|
195
|
+
const url = unsubscribeUrl(input.listUnsubscribeUrl);
|
|
196
|
+
if (url === null) {
|
|
197
|
+
return refuse('invalid_unsubscribe_url', 'The unsubscribe link must be an https URL.');
|
|
198
|
+
}
|
|
199
|
+
if (url) email.listUnsubscribeUrl = url;
|
|
200
|
+
const mailto = unsubscribeMailto(input.listUnsubscribeMailto);
|
|
201
|
+
if (mailto === null) {
|
|
202
|
+
return refuse('invalid_unsubscribe_mailto', 'The unsubscribe address is not an email address.');
|
|
203
|
+
}
|
|
204
|
+
if (mailto) email.listUnsubscribeMailto = mailto;
|
|
205
|
+
return {
|
|
206
|
+
email,
|
|
207
|
+
error: null,
|
|
208
|
+
unresolvedFields: unresolved
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
//# sourceMappingURL=compose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/compose.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 * WHAT EXACTLY IS SENT (AGL-2979).\n *\n * The composer turns a step, an enrollment and the organization's settings\n * into the message the transport sends: the recipient, the subject, the\n * plain text, and the headers that thread it and let the recipient leave.\n * The transport adds the From line and the MIME; nothing here knows either.\n *\n * ## The footer is not optional\n *\n * Every email ends with the organization's legal name, its postal address,\n * a sentence saying the email is a business solicitation, and the way out —\n * reply \"no\". Those are what CAN-SPAM requires of a commercial email, and\n * the composer appends them itself, after the merge, as literal text: no\n * template can leave them off, and no merge field can rewrite them. With no\n * postal address or no legal name, it refuses to compose at all.\n *\n * ## Threading\n *\n * An email after the first is a reply in the thread unless its step says\n * otherwise: `Re:` and the thread's own subject, `In-Reply-To` the last\n * message sent into the thread, `References` every one of them, and the\n * provider thread to send into. A `Re:` on an email that answers nothing is\n * a deception, so a reply step with no thread to answer is refused rather\n * than sent with a borrowed one, and a subject that starts with `Re:` or\n * `Fwd:` on an email that starts a thread is refused too.\n *\n * ## Merge fields\n *\n * The CRM's own resolver fills them, from the documents the runtime already\n * read, and the enrollment's personal line joins them as\n * `{{enrollment.personalLine}}` through the resolver's `extra` fields.\n * Fields that render empty are returned beside the message, for the\n * runtime to decide whether \"Hi ,\" is worth sending.\n *==========================================*/\n\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { CRM_EMAIL_SUBJECT_MAX } from '@aglyn/aglyn/app-utils/crm'\nimport {\n type CrmMergeContext,\n resolveCrmMergeFields,\n} from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { crmThreadSubject } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport type {\n OutreachEnrollment,\n OutreachOrgSettings,\n OutreachSequence,\n} from '../model/outreach.types'\nimport { normalizeOutreachPersonalLine } from './gates'\nimport {\n isInThreadEmailStep,\n OUTREACH_PERSONAL_LINE_FIELD,\n outreachSubjectHasReplyPrefix,\n} from './sequence-validation'\n\n/** The way out every email offers, word for word. */\nexport const OUTREACH_OPT_OUT_LINE = 'Not relevant? Reply \"no\" and I won\\'t email again.'\n\n/** The sentence that says what the email is. */\nexport function outreachSolicitationStatement(senderName: string): string {\n return `This is a business solicitation from ${senderName}.`\n}\n\n/**\n * The message the transport sends. Header values are single lines already;\n * the text uses `\\n` line breaks, which the transport writes as CRLF.\n */\nexport interface ComposedOutreachEmail {\n /** The recipient's address, normalized. */\n to: string\n subject: string\n /** The plain-text body, footer included. */\n text: string\n /** The provider thread a reply step is sent into. */\n threadId?: string\n /** The `Message-ID` this email answers, angle brackets included. */\n inReplyTo?: string\n /** Every `Message-ID` in the thread so far, oldest first, space-separated. */\n references?: string\n /** The one-click unsubscribe URL, for `List-Unsubscribe` with `List-Unsubscribe-Post`. */\n listUnsubscribeUrl?: string\n /** A `mailto:` URI for `List-Unsubscribe`. */\n listUnsubscribeMailto?: string\n}\n\nexport type OutreachComposeErrorCode =\n | 'missing_postal_address'\n | 'missing_legal_name'\n | 'not_an_email_step'\n | 'invalid_recipient'\n | 'missing_subject'\n | 'subject_reply_prefix'\n | 'missing_body'\n | 'missing_thread'\n | 'invalid_unsubscribe_url'\n | 'invalid_unsubscribe_mailto'\n\nexport interface OutreachComposeError {\n code: OutreachComposeErrorCode\n message: string\n}\n\nexport interface ComposeOutreachEmailInput {\n sequence: Pick<OutreachSequence, 'steps'>\n enrollment: Pick<\n OutreachEnrollment,\n 'email' | 'stepIndex' | 'personalLine' | 'threadSubject' | 'messageIds' | 'gmailThreadId'\n >\n orgSettings: Pick<OutreachOrgSettings, 'legalName' | 'brandName' | 'postalAddress'> | null | undefined\n /**\n * What the CRM resolver reads: the contact, the sending site's group id,\n * the sender (the mailbox's display name and address), the site. Its\n * `extra` may carry fields of the caller's own; the personal line is added\n * here.\n */\n merge: CrmMergeContext | null | undefined\n /** The CRM template's body, read by the caller, when the step names a template. */\n templateBody?: string | null\n /** The signed one-click unsubscribe URL, minted by the runtime. */\n listUnsubscribeUrl?: string | null\n /** An address, or a `mailto:` URI, that unsubscribes whoever writes to it. */\n listUnsubscribeMailto?: string | null\n}\n\nexport interface OutreachComposeResult {\n email: ComposedOutreachEmail | null\n error: OutreachComposeError | null\n /** Merge fields that rendered empty, unique, in order of appearance. */\n unresolvedFields: string[]\n}\n\nconst oneLine = (value: unknown): string =>\n typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n\n/**\n * The footer every email ends with, or the reason it cannot be written.\n *\n * The postal address may be stored over several lines; the footer prints it\n * on one, the parts separated by commas.\n */\nexport function composeOutreachFooter(\n settings: Pick<OutreachOrgSettings, 'legalName' | 'brandName' | 'postalAddress'> | null | undefined,\n): { footer: string | null; error: OutreachComposeError | null } {\n const postalAddress = String(settings?.postalAddress ?? '')\n .split(/\\r\\n|\\r|\\n/)\n .map((line) => oneLine(line).replace(/,+$/, ''))\n .filter(Boolean)\n .join(', ')\n if (!postalAddress) {\n return {\n footer: null,\n error: {\n code: 'missing_postal_address',\n message:\n \"This email can't be sent without your organization's postal address. Add it in Sequences settings.\",\n },\n }\n }\n const legalName = oneLine(settings?.legalName)\n if (!legalName) {\n return {\n footer: null,\n error: {\n code: 'missing_legal_name',\n message:\n \"This email can't be sent without your organization's legal name. Add it in Sequences settings.\",\n },\n }\n }\n const senderName = oneLine(settings?.brandName) || legalName\n return {\n footer: `${legalName} · ${postalAddress}\\n${outreachSolicitationStatement(senderName)} ${OUTREACH_OPT_OUT_LINE}`,\n error: null,\n }\n}\n\n/** A `Message-ID` in its header form — `<id@host>` — or `null` for one that is not. */\nexport function outreachMessageIdHeader(value: unknown): string | null {\n const bare = String(value ?? '').trim().replace(/^<+/, '').replace(/>+$/, '')\n if (!bare || /[\\s<>]/.test(bare)) return null\n return `<${bare}>`\n}\n\nfunction unsubscribeUrl(value: unknown): string | null | undefined {\n const raw = String(value ?? '').trim()\n if (!raw) return undefined\n if (/[\\r\\n]/.test(raw)) return null\n try {\n const url = new URL(raw)\n return url.protocol === 'https:' ? url.href : null\n } catch {\n return null\n }\n}\n\nfunction unsubscribeMailto(value: unknown): string | null | undefined {\n const raw = String(value ?? '').trim()\n if (!raw) return undefined\n if (/[\\r\\n]/.test(raw)) return null\n if (!/^mailto:/i.test(raw)) {\n const address = normalizeContactEmail(raw)\n return address ? `mailto:${address}?subject=unsubscribe` : null\n }\n let target = raw.slice('mailto:'.length).split('?')[0]\n try {\n target = decodeURIComponent(target)\n } catch {\n return null\n }\n return normalizeContactEmail(target) ? raw : null\n}\n\nconst refuse = (\n code: OutreachComposeErrorCode,\n message: string,\n): OutreachComposeResult => ({ email: null, error: { code, message }, unresolvedFields: [] })\n\n/** The email one step sends to one person — see the module note. */\nexport function composeOutreachEmail(input: ComposeOutreachEmailInput): OutreachComposeResult {\n const { footer, error } = composeOutreachFooter(input.orgSettings)\n if (error) return { email: null, error, unresolvedFields: [] }\n\n const steps = input.sequence?.steps ?? []\n const stepIndex = input.enrollment?.stepIndex\n const step = steps[stepIndex]\n if (step?.kind !== 'email') {\n return refuse('not_an_email_step', 'The step this enrollment is on does not send an email.')\n }\n const to = normalizeContactEmail(input.enrollment.email)\n if (!to) {\n return refuse('invalid_recipient', \"This enrollment's address isn't a valid email address.\")\n }\n\n const context: CrmMergeContext = {\n ...(input.merge ?? {}),\n extra: {\n ...(input.merge?.extra ?? {}),\n [OUTREACH_PERSONAL_LINE_FIELD]: normalizeOutreachPersonalLine(input.enrollment.personalLine),\n },\n }\n const unresolved: string[] = []\n const render = (text: string): string => {\n const result = resolveCrmMergeFields(text, context)\n for (const key of result.unresolved) if (!unresolved.includes(key)) unresolved.push(key)\n return result.text\n }\n\n const email: ComposedOutreachEmail = { to, subject: '', text: '' }\n if (isInThreadEmailStep(steps, stepIndex)) {\n const threadSubject = crmThreadSubject(input.enrollment.threadSubject)\n const messageIds = (input.enrollment.messageIds ?? [])\n .map(outreachMessageIdHeader)\n .filter((id): id is string => id !== null)\n const threadId = String(input.enrollment.gmailThreadId ?? '').trim()\n if (!threadSubject || !messageIds.length || !threadId) {\n return refuse(\n 'missing_thread',\n 'This email replies in the thread, and no earlier email in the thread was sent.',\n )\n }\n email.subject = `Re: ${threadSubject}`.slice(0, CRM_EMAIL_SUBJECT_MAX)\n email.threadId = threadId\n email.inReplyTo = messageIds[messageIds.length - 1]\n email.references = messageIds.join(' ')\n } else {\n const subject = oneLine(render(String(step.subject ?? '')))\n if (!subject) return refuse('missing_subject', 'This email has no subject.')\n if (outreachSubjectHasReplyPrefix(subject)) {\n return refuse(\n 'subject_reply_prefix',\n 'This email starts a thread, so its subject may not begin with \"Re:\" or \"Fwd:\".',\n )\n }\n email.subject = subject.slice(0, CRM_EMAIL_SUBJECT_MAX)\n }\n\n const source = step.templateId ? String(input.templateBody ?? '') : String(step.body ?? '')\n const body = render(source)\n .replace(/\\r\\n?/g, '\\n')\n .split('\\n')\n .map((line) => line.replace(/[ \\t]+$/, ''))\n .join('\\n')\n .replace(/^\\n+|\\n+$/g, '')\n if (!body.trim()) return refuse('missing_body', 'This email has no body.')\n email.text = `${body}\\n\\n${footer}`\n\n const url = unsubscribeUrl(input.listUnsubscribeUrl)\n if (url === null) {\n return refuse('invalid_unsubscribe_url', 'The unsubscribe link must be an https URL.')\n }\n if (url) email.listUnsubscribeUrl = url\n const mailto = unsubscribeMailto(input.listUnsubscribeMailto)\n if (mailto === null) {\n return refuse('invalid_unsubscribe_mailto', 'The unsubscribe address is not an email address.')\n }\n if (mailto) email.listUnsubscribeMailto = mailto\n\n return { email, error: null, unresolvedFields: unresolved }\n}\n"],"names":["normalizeContactEmail","CRM_EMAIL_SUBJECT_MAX","resolveCrmMergeFields","crmThreadSubject","normalizeOutreachPersonalLine","isInThreadEmailStep","OUTREACH_PERSONAL_LINE_FIELD","outreachSubjectHasReplyPrefix","OUTREACH_OPT_OUT_LINE","outreachSolicitationStatement","senderName","oneLine","value","replace","trim","composeOutreachFooter","settings","postalAddress","String","split","map","line","filter","Boolean","join","footer","error","code","message","legalName","brandName","outreachMessageIdHeader","bare","test","unsubscribeUrl","raw","undefined","url","URL","protocol","href","unsubscribeMailto","address","target","slice","length","decodeURIComponent","refuse","email","unresolvedFields","composeOutreachEmail","input","step","orgSettings","steps","sequence","stepIndex","enrollment","kind","to","context","merge","extra","personalLine","unresolved","render","text","result","key","includes","push","subject","threadSubject","messageIds","id","threadId","gmailThreadId","inReplyTo","references","source","templateId","templateBody","body","listUnsubscribeUrl","mailto","listUnsubscribeMailto"],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
import { type OutreachThreadMessage } from './thread-message';
|
|
18
|
+
export type OutreachBounceKind = 'hard' | 'soft';
|
|
19
|
+
/** What a delivery report says about one recipient. */
|
|
20
|
+
export interface OutreachDeliveryRecipient {
|
|
21
|
+
/** `Final-Recipient`, or `Original-Recipient` when that is all there is; normalized. */
|
|
22
|
+
address: string | null;
|
|
23
|
+
/** `Action`, lowercased: `failed`, `delayed`, `delivered`, `relayed`, `expanded`. */
|
|
24
|
+
action: string | null;
|
|
25
|
+
/** `Status`: the enhanced code, `5.1.1`. */
|
|
26
|
+
status: string | null;
|
|
27
|
+
/** `Diagnostic-Code`, without its type prefix. */
|
|
28
|
+
diagnostic: string | null;
|
|
29
|
+
/** `hard`, `soft`, or `null` for a block that reports no failure. */
|
|
30
|
+
kind: OutreachBounceKind | null;
|
|
31
|
+
}
|
|
32
|
+
export interface OutreachDeliveryReport {
|
|
33
|
+
/** The worst failure the report carries, or `null` when it reports none. */
|
|
34
|
+
kind: OutreachBounceKind | null;
|
|
35
|
+
/** Every recipient the report names, in order. */
|
|
36
|
+
recipients: OutreachDeliveryRecipient[];
|
|
37
|
+
/**
|
|
38
|
+
* The addresses `kind` is about, normalized — `X-Failed-Recipients` when no
|
|
39
|
+
* block names one. Empty when nothing says which recipient failed.
|
|
40
|
+
*/
|
|
41
|
+
failedAddresses: string[];
|
|
42
|
+
/** The status code of the worst failure, when one was found. */
|
|
43
|
+
status: string | null;
|
|
44
|
+
/** The diagnostic of the worst failure, when one was given. */
|
|
45
|
+
diagnostic: string | null;
|
|
46
|
+
}
|
|
47
|
+
/** Whether a message comes from a mail system rather than from a person. */
|
|
48
|
+
export declare function isMailerDaemonMessage(message: Pick<OutreachThreadMessage, 'from'>): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* What a delivery report says, or `null` when the message is not one — see
|
|
51
|
+
* the module note for how a standard report and a prose bounce are read.
|
|
52
|
+
*/
|
|
53
|
+
export declare function readOutreachDeliveryReport(message: OutreachThreadMessage): OutreachDeliveryReport | null;
|