@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,220 @@
|
|
|
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
|
+
* A REPLY THAT SAYS STOP (AGL-2979).
|
|
18
|
+
*
|
|
19
|
+
* Every Outreach email tells its reader to reply "no" to be left alone, so
|
|
20
|
+
* a reply has to be read for that. Any reply already stops its sequence;
|
|
21
|
+
* reading one as an OPT-OUT also puts the address on the organization's
|
|
22
|
+
* do-not-contact list, so no later sequence reaches it either. Both mistakes
|
|
23
|
+
* cost something: a missed opt-out lets another sequence write to somebody
|
|
24
|
+
* who said no, and a false one silently loses a prospect who said "no rush".
|
|
25
|
+
*
|
|
26
|
+
* ## Only the reply's own words
|
|
27
|
+
*
|
|
28
|
+
* A reply quotes the email it answers, and that email's footer says "Reply
|
|
29
|
+
* 'no'". So the quoted history is cut first — the CRM capture reader's
|
|
30
|
+
* `stripQuotedHistory` — along with a signature and any footer line a mail
|
|
31
|
+
* client quoted without marking it.
|
|
32
|
+
*
|
|
33
|
+
* ## What counts
|
|
34
|
+
*
|
|
35
|
+
* - A short reply that says nothing but no: "No.", "No thanks", "Nope",
|
|
36
|
+
* "Pass", "Stop" — with a greeting and a sign-off allowed around it.
|
|
37
|
+
* - A request anywhere in the reply: unsubscribe, remove me, take me off
|
|
38
|
+
* the list, stop emailing, don't contact me, opt out, no more emails —
|
|
39
|
+
* unless it is negated ("please don't remove me").
|
|
40
|
+
* - "Not interested", unless it defers ("not interested yet", "not
|
|
41
|
+
* interested right now, try me next quarter") or asks ("not interested?").
|
|
42
|
+
* - A reply that calls the email spam, which is also a COMPLAINT.
|
|
43
|
+
*
|
|
44
|
+
* "No rush", "no problem" and "no worries" are none of these: a "no" only
|
|
45
|
+
* counts when it is the whole answer.
|
|
46
|
+
*==========================================*/ import { stripQuotedHistory } from "@aglyn/aglyn/app-utils/crm-inbound";
|
|
47
|
+
const NONE = {
|
|
48
|
+
optOut: false,
|
|
49
|
+
complaint: false,
|
|
50
|
+
matched: null
|
|
51
|
+
};
|
|
52
|
+
/** A line of the footer every Outreach email carries, however a client re-quoted it. */ const FOOTER_LINE = /reply\s+["“”'‘’]?no["“”'‘’]?\s+and\s+i\s+won['’]?t\s+email|business\s+solicitation\s+from/i;
|
|
53
|
+
/**
|
|
54
|
+
* The words a reply adds to its thread: the text above the quoted history,
|
|
55
|
+
* with a signature and any quoted footer line removed.
|
|
56
|
+
*/ export function outreachFreshReplyText(text) {
|
|
57
|
+
const body = stripQuotedHistory(String(text != null ? text : '').replace(/\r\n?/g, '\n').replace(/[\u00a0\u202f]/g, ' '));
|
|
58
|
+
const lines = body.split('\n');
|
|
59
|
+
const signature = lines.findIndex((line)=>/^--\s*$/.test(line));
|
|
60
|
+
return (signature >= 0 ? lines.slice(0, signature) : lines).filter((line)=>!FOOTER_LINE.test(line)).join('\n').trim();
|
|
61
|
+
}
|
|
62
|
+
/** Curly quotes straightened, case folded, whitespace settled. */ function fold(text) {
|
|
63
|
+
return text.replace(/[‘’]/g, "'").replace(/[“”]/g, '"').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
64
|
+
}
|
|
65
|
+
const GREETING_LINE = /^(hi|hello|hey|dear|greetings|good (morning|afternoon|evening))\b[^\n]{0,40}$/i;
|
|
66
|
+
const GREETING_LEAD = /^(hi|hello|hey|dear|greetings)(\s+[a-z'’-]+)?\s*[,!.:–—-]\s*/i;
|
|
67
|
+
const SIGN_OFF = /^(thanks|thank you|thx|many thanks|cheers|regards|best|best regards|kind regards|warm regards|sincerely|all the best|sent from my\b.*|get outlook\b.*)[\s,.!]*$/i;
|
|
68
|
+
const NAME = "[A-Z][a-z'’-]+( [A-Z][a-z'’-]+)?";
|
|
69
|
+
/** Thanks, then a signed name: "No thanks, Jordan" or "No thanks — Jordan". */ const THANKS_THEN_NAME = new RegExp(`\\b(thanks|thank you|thx|cheers)[\\s,–—-]+${NAME}[.!]?\\s*$`, 'i');
|
|
70
|
+
const NAME_LINE = new RegExp(`^${NAME}[.!]?$`);
|
|
71
|
+
/**
|
|
72
|
+
* What is left of a reply once its greeting, its sign-off and a name signed
|
|
73
|
+
* after "thanks" are gone. A name is only taken off after a thank-you,
|
|
74
|
+
* because "No, WordPress." is an answer about a platform, not a signature.
|
|
75
|
+
*/ function answerOnly(fresh) {
|
|
76
|
+
const lines = fresh.split('\n').map((line)=>line.trim()).filter(Boolean);
|
|
77
|
+
if (lines.length > 1 && GREETING_LINE.test(lines[0])) lines.shift();
|
|
78
|
+
const signOff = lines.findIndex((line, index)=>index > 0 && SIGN_OFF.test(line));
|
|
79
|
+
const kept = signOff >= 0 ? lines.slice(0, signOff) : lines;
|
|
80
|
+
if (kept.length > 1 && NAME_LINE.test(kept[kept.length - 1]) && /\b(thanks|thank you|thx|cheers)[\s,.!]*$/i.test(kept[kept.length - 2])) {
|
|
81
|
+
kept.pop();
|
|
82
|
+
}
|
|
83
|
+
return kept.join(' ').replace(GREETING_LEAD, '').replace(THANKS_THEN_NAME, '$1');
|
|
84
|
+
}
|
|
85
|
+
/** The only words a reply may hold to be read as a bare "no". */ const BARE_NO_WORDS = new Set([
|
|
86
|
+
'no',
|
|
87
|
+
'nope',
|
|
88
|
+
'nah',
|
|
89
|
+
'pass',
|
|
90
|
+
"i'll",
|
|
91
|
+
'ill',
|
|
92
|
+
'i',
|
|
93
|
+
'will',
|
|
94
|
+
'stop',
|
|
95
|
+
'unsubscribe',
|
|
96
|
+
'remove',
|
|
97
|
+
'me',
|
|
98
|
+
'us',
|
|
99
|
+
'please',
|
|
100
|
+
'thanks',
|
|
101
|
+
'thank',
|
|
102
|
+
'you',
|
|
103
|
+
'thx',
|
|
104
|
+
'sorry',
|
|
105
|
+
'but',
|
|
106
|
+
'not',
|
|
107
|
+
'for',
|
|
108
|
+
'interested'
|
|
109
|
+
]);
|
|
110
|
+
const BARE_NO_TRIGGERS = new Set([
|
|
111
|
+
'no',
|
|
112
|
+
'nope',
|
|
113
|
+
'nah',
|
|
114
|
+
'pass',
|
|
115
|
+
'stop',
|
|
116
|
+
'unsubscribe',
|
|
117
|
+
'remove'
|
|
118
|
+
]);
|
|
119
|
+
function bareNo(answer) {
|
|
120
|
+
const words = fold(answer).replace(/[^a-z' ]+/g, ' ').split(' ').filter(Boolean);
|
|
121
|
+
if (!words.length || words.length > 6) return null;
|
|
122
|
+
if (!words.every((word)=>BARE_NO_WORDS.has(word))) return null;
|
|
123
|
+
const said = words.join(' ');
|
|
124
|
+
if (words.some((word)=>BARE_NO_TRIGGERS.has(word)) || /\bnot interested\b|\bnot for (me|us)\b/.test(said)) {
|
|
125
|
+
return said;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
/** Negations that turn a request around when they come just before it. */ const NEGATED = /\b(don't|dont|do not|didn't|didnt|did not|never|not|no need to|please don't|wouldn't|won't)\s+(\w+\s+){0,2}$/;
|
|
130
|
+
const REQUESTS = [
|
|
131
|
+
/\bunsubscrib(e|ed|ing)\b/,
|
|
132
|
+
/\bopt(ed|ing)?[ -]?(me )?out\b/,
|
|
133
|
+
/\bremove (me|us|my (email|e-mail|address|name|details|info))\b/,
|
|
134
|
+
/\bremove\b[^.!?\n]{0,30}\bfrom (your|the|this|all) (list|lists|mailing list|emails?|database|system|sequence)\b/,
|
|
135
|
+
/\btake (me|us|my (email|e-mail|address|name)) off\b/,
|
|
136
|
+
/\btake\b[^.!?\n]{0,30}\boff (your|the|this|all) (list|lists|mailing list|emails?)\b/,
|
|
137
|
+
// Aimed at us: "stop emailing me", "stop sending these", "stop contacting." —
|
|
138
|
+
// not "we had to stop sending newsletters ourselves".
|
|
139
|
+
/\b(stop|quit) (emailing|e-mailing|mailing|contacting|messaging|writing|sending|reaching out)(?=\s*([.!,;]|$)|\s+(me|us|my|our|this|these|those|them|to (me|us)|emails?|e-mails?|messages?|mail)\b)/,
|
|
140
|
+
/\bplease stop\b/,
|
|
141
|
+
/^stop\b(?! (by|in|over|at|the|thinking|worrying))/,
|
|
142
|
+
/\b(do not|don't|dont|never) (email|e-mail|contact|message|write to|reach out to) (me|us)\b/,
|
|
143
|
+
/\bno (more|further) (emails?|e-mails?|messages|mail|contact)\b/,
|
|
144
|
+
/\b(don't|do not) want (to (hear|receive)|any more|these)\b[^.!?\n]{0,20}\b(emails?|messages|from you)\b/
|
|
145
|
+
];
|
|
146
|
+
const COMPLAINTS = [
|
|
147
|
+
/\b(this|that|it|you|your emails?|these emails?|this email)\s*(is|are|'s|'re)\s+(just\s+)?spam\b/,
|
|
148
|
+
/\b(stop|quit|don't|do not) spam(ming)?\b/,
|
|
149
|
+
/\bspamming (me|us|my|our)\b/,
|
|
150
|
+
/\b(i|we)('ve| have| just| will| am| are)?\s*(report(ed|ing)?|mark(ed|ing)?|flag(ged|ging)?)\b[^.!?\n]{0,30}\b(as|for) spam\b/
|
|
151
|
+
];
|
|
152
|
+
/** Words after "not interested" that put it off rather than rule it out. */ const DEFERRAL = /^[\s,]*(yet|right now|for now|now|at the moment|at this (time|moment|point|stage)|today|this (week|month|quarter|year)|until|in the (near )?future|but\b|however\b|though\b)/;
|
|
153
|
+
function notInterested(text) {
|
|
154
|
+
const pattern = /\bnot (really |at all )?interested\b/g;
|
|
155
|
+
for(let match = pattern.exec(text); match; match = pattern.exec(text)){
|
|
156
|
+
const before = text.slice(0, match.index);
|
|
157
|
+
const after = text.slice(match.index + match[0].length);
|
|
158
|
+
const sentenceEnd = after.search(/[.!?\n]/);
|
|
159
|
+
const rest = sentenceEnd >= 0 ? after.slice(0, sentenceEnd + 1) : after;
|
|
160
|
+
if (rest.trimEnd().endsWith('?')) continue;
|
|
161
|
+
if (DEFERRAL.test(after)) continue;
|
|
162
|
+
if (/\b(if|unless|in case|are you|were you|you're|you are)\s+(\w+\s+){0,2}$/.test(before)) continue;
|
|
163
|
+
return match[0];
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Whether a SUBJECT says nothing but stop — "Unsubscribe", "Remove me",
|
|
169
|
+
* "STOP" — as a message sent from an unsubscribe link, or typed fresh, does.
|
|
170
|
+
* Only a bare answer counts: a subject is a sender's headline, and "Stop
|
|
171
|
+
* losing leads" is one.
|
|
172
|
+
*/ export function detectOutreachOptOutSubject(subject) {
|
|
173
|
+
const said = bareNo(String(subject != null ? subject : ''));
|
|
174
|
+
return said ? {
|
|
175
|
+
optOut: true,
|
|
176
|
+
complaint: false,
|
|
177
|
+
matched: said
|
|
178
|
+
} : NONE;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Whether a reply's text asks to be left alone — see the module note. Pass
|
|
182
|
+
* the reply's text as received; the quoted history is removed here.
|
|
183
|
+
*/ export function detectOutreachOptOutIntent(text) {
|
|
184
|
+
const fresh = outreachFreshReplyText(text);
|
|
185
|
+
if (!fresh) return NONE;
|
|
186
|
+
const folded = fold(fresh);
|
|
187
|
+
for (const pattern of COMPLAINTS){
|
|
188
|
+
const match = pattern.exec(folded);
|
|
189
|
+
if (match) return {
|
|
190
|
+
optOut: true,
|
|
191
|
+
complaint: true,
|
|
192
|
+
matched: match[0]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const bare = bareNo(answerOnly(fresh));
|
|
196
|
+
if (bare) return {
|
|
197
|
+
optOut: true,
|
|
198
|
+
complaint: false,
|
|
199
|
+
matched: bare
|
|
200
|
+
};
|
|
201
|
+
for (const pattern of REQUESTS){
|
|
202
|
+
const match = pattern.exec(folded);
|
|
203
|
+
if (match && !NEGATED.test(folded.slice(0, match.index))) {
|
|
204
|
+
return {
|
|
205
|
+
optOut: true,
|
|
206
|
+
complaint: false,
|
|
207
|
+
matched: match[0]
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const declined = notInterested(folded);
|
|
212
|
+
if (declined) return {
|
|
213
|
+
optOut: true,
|
|
214
|
+
complaint: false,
|
|
215
|
+
matched: declined
|
|
216
|
+
};
|
|
217
|
+
return NONE;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
//# sourceMappingURL=opt-out-intent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/opt-out-intent.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 * A REPLY THAT SAYS STOP (AGL-2979).\n *\n * Every Outreach email tells its reader to reply \"no\" to be left alone, so\n * a reply has to be read for that. Any reply already stops its sequence;\n * reading one as an OPT-OUT also puts the address on the organization's\n * do-not-contact list, so no later sequence reaches it either. Both mistakes\n * cost something: a missed opt-out lets another sequence write to somebody\n * who said no, and a false one silently loses a prospect who said \"no rush\".\n *\n * ## Only the reply's own words\n *\n * A reply quotes the email it answers, and that email's footer says \"Reply\n * 'no'\". So the quoted history is cut first — the CRM capture reader's\n * `stripQuotedHistory` — along with a signature and any footer line a mail\n * client quoted without marking it.\n *\n * ## What counts\n *\n * - A short reply that says nothing but no: \"No.\", \"No thanks\", \"Nope\",\n * \"Pass\", \"Stop\" — with a greeting and a sign-off allowed around it.\n * - A request anywhere in the reply: unsubscribe, remove me, take me off\n * the list, stop emailing, don't contact me, opt out, no more emails —\n * unless it is negated (\"please don't remove me\").\n * - \"Not interested\", unless it defers (\"not interested yet\", \"not\n * interested right now, try me next quarter\") or asks (\"not interested?\").\n * - A reply that calls the email spam, which is also a COMPLAINT.\n *\n * \"No rush\", \"no problem\" and \"no worries\" are none of these: a \"no\" only\n * counts when it is the whole answer.\n *==========================================*/\n\nimport { stripQuotedHistory } from '@aglyn/aglyn/app-utils/crm-inbound'\n\nexport interface OutreachOptOutIntent {\n /** The reply asks not to be emailed again. */\n optOut: boolean\n /** The reply calls the email spam — an opt-out that also counts against the mailbox. */\n complaint: boolean\n /** The words that decided it, for the timeline and the log; `null` when nothing matched. */\n matched: string | null\n}\n\nconst NONE: OutreachOptOutIntent = { optOut: false, complaint: false, matched: null }\n\n/** A line of the footer every Outreach email carries, however a client re-quoted it. */\nconst FOOTER_LINE = /reply\\s+[\"“”'‘’]?no[\"“”'‘’]?\\s+and\\s+i\\s+won['’]?t\\s+email|business\\s+solicitation\\s+from/i\n\n/**\n * The words a reply adds to its thread: the text above the quoted history,\n * with a signature and any quoted footer line removed.\n */\nexport function outreachFreshReplyText(text: unknown): string {\n const body = stripQuotedHistory(\n String(text ?? '')\n .replace(/\\r\\n?/g, '\\n')\n .replace(/[\\u00a0\\u202f]/g, ' '),\n )\n const lines = body.split('\\n')\n const signature = lines.findIndex((line) => /^--\\s*$/.test(line))\n return (signature >= 0 ? lines.slice(0, signature) : lines)\n .filter((line) => !FOOTER_LINE.test(line))\n .join('\\n')\n .trim()\n}\n\n/** Curly quotes straightened, case folded, whitespace settled. */\nfunction fold(text: string): string {\n return text\n .replace(/[‘’]/g, \"'\")\n .replace(/[“”]/g, '\"')\n .toLowerCase()\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\nconst GREETING_LINE = /^(hi|hello|hey|dear|greetings|good (morning|afternoon|evening))\\b[^\\n]{0,40}$/i\nconst GREETING_LEAD = /^(hi|hello|hey|dear|greetings)(\\s+[a-z'’-]+)?\\s*[,!.:–—-]\\s*/i\nconst SIGN_OFF =\n /^(thanks|thank you|thx|many thanks|cheers|regards|best|best regards|kind regards|warm regards|sincerely|all the best|sent from my\\b.*|get outlook\\b.*)[\\s,.!]*$/i\nconst NAME = \"[A-Z][a-z'’-]+( [A-Z][a-z'’-]+)?\"\n/** Thanks, then a signed name: \"No thanks, Jordan\" or \"No thanks — Jordan\". */\nconst THANKS_THEN_NAME = new RegExp(`\\\\b(thanks|thank you|thx|cheers)[\\\\s,–—-]+${NAME}[.!]?\\\\s*$`, 'i')\nconst NAME_LINE = new RegExp(`^${NAME}[.!]?$`)\n\n/**\n * What is left of a reply once its greeting, its sign-off and a name signed\n * after \"thanks\" are gone. A name is only taken off after a thank-you,\n * because \"No, WordPress.\" is an answer about a platform, not a signature.\n */\nfunction answerOnly(fresh: string): string {\n const lines = fresh\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean)\n if (lines.length > 1 && GREETING_LINE.test(lines[0])) lines.shift()\n const signOff = lines.findIndex((line, index) => index > 0 && SIGN_OFF.test(line))\n const kept = signOff >= 0 ? lines.slice(0, signOff) : lines\n if (\n kept.length > 1 &&\n NAME_LINE.test(kept[kept.length - 1]) &&\n /\\b(thanks|thank you|thx|cheers)[\\s,.!]*$/i.test(kept[kept.length - 2])\n ) {\n kept.pop()\n }\n return kept\n .join(' ')\n .replace(GREETING_LEAD, '')\n .replace(THANKS_THEN_NAME, '$1')\n}\n\n/** The only words a reply may hold to be read as a bare \"no\". */\nconst BARE_NO_WORDS = new Set([\n 'no',\n 'nope',\n 'nah',\n 'pass',\n \"i'll\",\n 'ill',\n 'i',\n 'will',\n 'stop',\n 'unsubscribe',\n 'remove',\n 'me',\n 'us',\n 'please',\n 'thanks',\n 'thank',\n 'you',\n 'thx',\n 'sorry',\n 'but',\n 'not',\n 'for',\n 'interested',\n])\nconst BARE_NO_TRIGGERS = new Set(['no', 'nope', 'nah', 'pass', 'stop', 'unsubscribe', 'remove'])\n\nfunction bareNo(answer: string): string | null {\n const words = fold(answer)\n .replace(/[^a-z' ]+/g, ' ')\n .split(' ')\n .filter(Boolean)\n if (!words.length || words.length > 6) return null\n if (!words.every((word) => BARE_NO_WORDS.has(word))) return null\n const said = words.join(' ')\n if (\n words.some((word) => BARE_NO_TRIGGERS.has(word)) ||\n /\\bnot interested\\b|\\bnot for (me|us)\\b/.test(said)\n ) {\n return said\n }\n return null\n}\n\n/** Negations that turn a request around when they come just before it. */\nconst NEGATED = /\\b(don't|dont|do not|didn't|didnt|did not|never|not|no need to|please don't|wouldn't|won't)\\s+(\\w+\\s+){0,2}$/\n\nconst REQUESTS: readonly RegExp[] = [\n /\\bunsubscrib(e|ed|ing)\\b/,\n /\\bopt(ed|ing)?[ -]?(me )?out\\b/,\n /\\bremove (me|us|my (email|e-mail|address|name|details|info))\\b/,\n /\\bremove\\b[^.!?\\n]{0,30}\\bfrom (your|the|this|all) (list|lists|mailing list|emails?|database|system|sequence)\\b/,\n /\\btake (me|us|my (email|e-mail|address|name)) off\\b/,\n /\\btake\\b[^.!?\\n]{0,30}\\boff (your|the|this|all) (list|lists|mailing list|emails?)\\b/,\n // Aimed at us: \"stop emailing me\", \"stop sending these\", \"stop contacting.\" —\n // not \"we had to stop sending newsletters ourselves\".\n /\\b(stop|quit) (emailing|e-mailing|mailing|contacting|messaging|writing|sending|reaching out)(?=\\s*([.!,;]|$)|\\s+(me|us|my|our|this|these|those|them|to (me|us)|emails?|e-mails?|messages?|mail)\\b)/,\n /\\bplease stop\\b/,\n /^stop\\b(?! (by|in|over|at|the|thinking|worrying))/,\n /\\b(do not|don't|dont|never) (email|e-mail|contact|message|write to|reach out to) (me|us)\\b/,\n /\\bno (more|further) (emails?|e-mails?|messages|mail|contact)\\b/,\n /\\b(don't|do not) want (to (hear|receive)|any more|these)\\b[^.!?\\n]{0,20}\\b(emails?|messages|from you)\\b/,\n]\n\nconst COMPLAINTS: readonly RegExp[] = [\n /\\b(this|that|it|you|your emails?|these emails?|this email)\\s*(is|are|'s|'re)\\s+(just\\s+)?spam\\b/,\n /\\b(stop|quit|don't|do not) spam(ming)?\\b/,\n /\\bspamming (me|us|my|our)\\b/,\n /\\b(i|we)('ve| have| just| will| am| are)?\\s*(report(ed|ing)?|mark(ed|ing)?|flag(ged|ging)?)\\b[^.!?\\n]{0,30}\\b(as|for) spam\\b/,\n]\n\n/** Words after \"not interested\" that put it off rather than rule it out. */\nconst DEFERRAL =\n /^[\\s,]*(yet|right now|for now|now|at the moment|at this (time|moment|point|stage)|today|this (week|month|quarter|year)|until|in the (near )?future|but\\b|however\\b|though\\b)/\n\nfunction notInterested(text: string): string | null {\n const pattern = /\\bnot (really |at all )?interested\\b/g\n for (let match = pattern.exec(text); match; match = pattern.exec(text)) {\n const before = text.slice(0, match.index)\n const after = text.slice(match.index + match[0].length)\n const sentenceEnd = after.search(/[.!?\\n]/)\n const rest = sentenceEnd >= 0 ? after.slice(0, sentenceEnd + 1) : after\n if (rest.trimEnd().endsWith('?')) continue\n if (DEFERRAL.test(after)) continue\n if (/\\b(if|unless|in case|are you|were you|you're|you are)\\s+(\\w+\\s+){0,2}$/.test(before)) continue\n return match[0]\n }\n return null\n}\n\n/**\n * Whether a SUBJECT says nothing but stop — \"Unsubscribe\", \"Remove me\",\n * \"STOP\" — as a message sent from an unsubscribe link, or typed fresh, does.\n * Only a bare answer counts: a subject is a sender's headline, and \"Stop\n * losing leads\" is one.\n */\nexport function detectOutreachOptOutSubject(subject: unknown): OutreachOptOutIntent {\n const said = bareNo(String(subject ?? ''))\n return said ? { optOut: true, complaint: false, matched: said } : NONE\n}\n\n/**\n * Whether a reply's text asks to be left alone — see the module note. Pass\n * the reply's text as received; the quoted history is removed here.\n */\nexport function detectOutreachOptOutIntent(text: unknown): OutreachOptOutIntent {\n const fresh = outreachFreshReplyText(text)\n if (!fresh) return NONE\n const folded = fold(fresh)\n for (const pattern of COMPLAINTS) {\n const match = pattern.exec(folded)\n if (match) return { optOut: true, complaint: true, matched: match[0] }\n }\n const bare = bareNo(answerOnly(fresh))\n if (bare) return { optOut: true, complaint: false, matched: bare }\n for (const pattern of REQUESTS) {\n const match = pattern.exec(folded)\n if (match && !NEGATED.test(folded.slice(0, match.index))) {\n return { optOut: true, complaint: false, matched: match[0] }\n }\n }\n const declined = notInterested(folded)\n if (declined) return { optOut: true, complaint: false, matched: declined }\n return NONE\n}\n"],"names":["stripQuotedHistory","NONE","optOut","complaint","matched","FOOTER_LINE","outreachFreshReplyText","text","body","String","replace","lines","split","signature","findIndex","line","test","slice","filter","join","trim","fold","toLowerCase","GREETING_LINE","GREETING_LEAD","SIGN_OFF","NAME","THANKS_THEN_NAME","RegExp","NAME_LINE","answerOnly","fresh","map","Boolean","length","shift","signOff","index","kept","pop","BARE_NO_WORDS","Set","BARE_NO_TRIGGERS","bareNo","answer","words","every","word","has","said","some","NEGATED","REQUESTS","COMPLAINTS","DEFERRAL","notInterested","pattern","match","exec","before","after","sentenceEnd","search","rest","trimEnd","endsWith","detectOutreachOptOutSubject","subject","detectOutreachOptOutIntent","folded","bare","declined"],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
/** How the country was learned — which of the three sources answered. */
|
|
18
|
+
export type OutreachCountrySource = 'contact' | 'company' | 'domain';
|
|
19
|
+
export interface OutreachRecipientCountry {
|
|
20
|
+
/** ISO-3166-1 alpha-2, uppercase, or `null` when nothing names one. */
|
|
21
|
+
country: string | null;
|
|
22
|
+
source: OutreachCountrySource | null;
|
|
23
|
+
}
|
|
24
|
+
/** The country an address's domain names, or `null` for a generic one. */
|
|
25
|
+
export declare function countryFromEmailDomain(email: unknown): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* The recipient's country from the best source that names one — see the
|
|
28
|
+
* module note for the order. `contactAddress` is the address on the sending
|
|
29
|
+
* group's facet of the contact; `companyAddress` is the one on the company
|
|
30
|
+
* the contact is filed under.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveOutreachRecipientCountry(input: {
|
|
33
|
+
email: string;
|
|
34
|
+
contactAddress?: unknown;
|
|
35
|
+
companyAddress?: unknown;
|
|
36
|
+
}): OutreachRecipientCountry;
|
|
37
|
+
/**
|
|
38
|
+
* A country's English name as a sentence says it — "the United States",
|
|
39
|
+
* "Canada" — or the code itself where the runtime has no region names.
|
|
40
|
+
*/
|
|
41
|
+
export declare function outreachCountryName(code: string): string;
|
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
* WHERE A RECIPIENT IS (AGL-2979).
|
|
18
|
+
*
|
|
19
|
+
* A cold email's legality turns on the recipient's country: the United
|
|
20
|
+
* States allows one with an opt-out, while Canada, the United Kingdom and
|
|
21
|
+
* most of the EU want a consent basis a cold email does not have. The CRM
|
|
22
|
+
* rarely knows the country for certain, so the gate reads what it has in
|
|
23
|
+
* order of how much it is worth:
|
|
24
|
+
*
|
|
25
|
+
* 1. the country on the contact's own address, as the sending site's facet
|
|
26
|
+
* records it;
|
|
27
|
+
* 2. the country on the company the contact is filed under;
|
|
28
|
+
* 3. the country an address's domain names — `.ca`, `.co.uk`, `.de` — which
|
|
29
|
+
* is evidence against sending rather than for it: a `.de` address is
|
|
30
|
+
* almost always in Germany, while a `.com` address says nothing at all.
|
|
31
|
+
*
|
|
32
|
+
* Only country-code domains that are used for their country are read.
|
|
33
|
+
* `.io`, `.co`, `.ai`, `.me` and the rest that are sold as generic names are
|
|
34
|
+
* not, because reading `.co` as Colombia would refuse half of the startups a
|
|
35
|
+
* rep writes to for the wrong reason.
|
|
36
|
+
*==========================================*/ import { normalizeAddress } from "@aglyn/aglyn/foundation/definitions/contact.types";
|
|
37
|
+
/**
|
|
38
|
+
* Country-code top-level domains read as their country: the ones registries
|
|
39
|
+
* reserve for, or that are overwhelmingly used by, people and companies in
|
|
40
|
+
* that country. `.uk` is Great Britain's `GB`.
|
|
41
|
+
*/ const COUNTRY_CODE_DOMAINS = {
|
|
42
|
+
us: 'US',
|
|
43
|
+
ca: 'CA',
|
|
44
|
+
mx: 'MX',
|
|
45
|
+
br: 'BR',
|
|
46
|
+
ar: 'AR',
|
|
47
|
+
cl: 'CL',
|
|
48
|
+
uk: 'GB',
|
|
49
|
+
ie: 'IE',
|
|
50
|
+
de: 'DE',
|
|
51
|
+
at: 'AT',
|
|
52
|
+
ch: 'CH',
|
|
53
|
+
fr: 'FR',
|
|
54
|
+
be: 'BE',
|
|
55
|
+
nl: 'NL',
|
|
56
|
+
lu: 'LU',
|
|
57
|
+
es: 'ES',
|
|
58
|
+
pt: 'PT',
|
|
59
|
+
it: 'IT',
|
|
60
|
+
gr: 'GR',
|
|
61
|
+
se: 'SE',
|
|
62
|
+
no: 'NO',
|
|
63
|
+
dk: 'DK',
|
|
64
|
+
fi: 'FI',
|
|
65
|
+
pl: 'PL',
|
|
66
|
+
cz: 'CZ',
|
|
67
|
+
sk: 'SK',
|
|
68
|
+
hu: 'HU',
|
|
69
|
+
ro: 'RO',
|
|
70
|
+
bg: 'BG',
|
|
71
|
+
hr: 'HR',
|
|
72
|
+
si: 'SI',
|
|
73
|
+
ee: 'EE',
|
|
74
|
+
lt: 'LT',
|
|
75
|
+
lv: 'LV',
|
|
76
|
+
mt: 'MT',
|
|
77
|
+
cy: 'CY',
|
|
78
|
+
ru: 'RU',
|
|
79
|
+
ua: 'UA',
|
|
80
|
+
tr: 'TR',
|
|
81
|
+
il: 'IL',
|
|
82
|
+
ae: 'AE',
|
|
83
|
+
za: 'ZA',
|
|
84
|
+
in: 'IN',
|
|
85
|
+
cn: 'CN',
|
|
86
|
+
jp: 'JP',
|
|
87
|
+
kr: 'KR',
|
|
88
|
+
sg: 'SG',
|
|
89
|
+
hk: 'HK',
|
|
90
|
+
au: 'AU',
|
|
91
|
+
nz: 'NZ'
|
|
92
|
+
};
|
|
93
|
+
/** The country an address's domain names, or `null` for a generic one. */ export function countryFromEmailDomain(email) {
|
|
94
|
+
var _COUNTRY_CODE_DOMAINS_tld;
|
|
95
|
+
const value = String(email != null ? email : '').trim().toLowerCase();
|
|
96
|
+
const domain = value.slice(value.lastIndexOf('@') + 1).replace(/\.+$/, '');
|
|
97
|
+
const tld = domain.slice(domain.lastIndexOf('.') + 1);
|
|
98
|
+
if (!domain.includes('.') || !tld) return null;
|
|
99
|
+
return (_COUNTRY_CODE_DOMAINS_tld = COUNTRY_CODE_DOMAINS[tld]) != null ? _COUNTRY_CODE_DOMAINS_tld : null;
|
|
100
|
+
}
|
|
101
|
+
function addressCountry(address) {
|
|
102
|
+
var _ref;
|
|
103
|
+
var _normalizeAddress;
|
|
104
|
+
if (!address || typeof address !== 'object') return null;
|
|
105
|
+
return (_ref = (_normalizeAddress = normalizeAddress(address)) == null ? void 0 : _normalizeAddress.country) != null ? _ref : null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The recipient's country from the best source that names one — see the
|
|
109
|
+
* module note for the order. `contactAddress` is the address on the sending
|
|
110
|
+
* group's facet of the contact; `companyAddress` is the one on the company
|
|
111
|
+
* the contact is filed under.
|
|
112
|
+
*/ export function resolveOutreachRecipientCountry(input) {
|
|
113
|
+
const fromContact = addressCountry(input.contactAddress);
|
|
114
|
+
if (fromContact) return {
|
|
115
|
+
country: fromContact,
|
|
116
|
+
source: 'contact'
|
|
117
|
+
};
|
|
118
|
+
const fromCompany = addressCountry(input.companyAddress);
|
|
119
|
+
if (fromCompany) return {
|
|
120
|
+
country: fromCompany,
|
|
121
|
+
source: 'company'
|
|
122
|
+
};
|
|
123
|
+
const fromDomain = countryFromEmailDomain(input.email);
|
|
124
|
+
if (fromDomain) return {
|
|
125
|
+
country: fromDomain,
|
|
126
|
+
source: 'domain'
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
country: null,
|
|
130
|
+
source: null
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Countries whose English name is read with "the": "the United States". */ const DEFINITE_ARTICLE = new Set([
|
|
134
|
+
'US',
|
|
135
|
+
'GB',
|
|
136
|
+
'NL',
|
|
137
|
+
'AE',
|
|
138
|
+
'PH',
|
|
139
|
+
'DO',
|
|
140
|
+
'BS',
|
|
141
|
+
'GM'
|
|
142
|
+
]);
|
|
143
|
+
let regionNames;
|
|
144
|
+
/**
|
|
145
|
+
* A country's English name as a sentence says it — "the United States",
|
|
146
|
+
* "Canada" — or the code itself where the runtime has no region names.
|
|
147
|
+
*/ export function outreachCountryName(code) {
|
|
148
|
+
const upper = String(code != null ? code : '').toUpperCase();
|
|
149
|
+
if (regionNames === undefined) {
|
|
150
|
+
try {
|
|
151
|
+
regionNames = new Intl.DisplayNames([
|
|
152
|
+
'en'
|
|
153
|
+
], {
|
|
154
|
+
type: 'region'
|
|
155
|
+
});
|
|
156
|
+
} catch (unused) {
|
|
157
|
+
regionNames = null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
let name;
|
|
161
|
+
try {
|
|
162
|
+
var _ref;
|
|
163
|
+
name = (_ref = regionNames == null ? void 0 : regionNames.of(upper)) != null ? _ref : upper;
|
|
164
|
+
} catch (unused) {
|
|
165
|
+
// A code `Intl` rejects outright is printed as it came.
|
|
166
|
+
name = upper;
|
|
167
|
+
}
|
|
168
|
+
return DEFINITE_ARTICLE.has(upper) && name !== upper ? `the ${name}` : name;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
//# sourceMappingURL=recipient-country.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/recipient-country.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 * WHERE A RECIPIENT IS (AGL-2979).\n *\n * A cold email's legality turns on the recipient's country: the United\n * States allows one with an opt-out, while Canada, the United Kingdom and\n * most of the EU want a consent basis a cold email does not have. The CRM\n * rarely knows the country for certain, so the gate reads what it has in\n * order of how much it is worth:\n *\n * 1. the country on the contact's own address, as the sending site's facet\n * records it;\n * 2. the country on the company the contact is filed under;\n * 3. the country an address's domain names — `.ca`, `.co.uk`, `.de` — which\n * is evidence against sending rather than for it: a `.de` address is\n * almost always in Germany, while a `.com` address says nothing at all.\n *\n * Only country-code domains that are used for their country are read.\n * `.io`, `.co`, `.ai`, `.me` and the rest that are sold as generic names are\n * not, because reading `.co` as Colombia would refuse half of the startups a\n * rep writes to for the wrong reason.\n *==========================================*/\n\nimport {\n type AglynPostalAddress,\n normalizeAddress,\n} from '@aglyn/aglyn/foundation/definitions/contact.types'\n\n/** How the country was learned — which of the three sources answered. */\nexport type OutreachCountrySource = 'contact' | 'company' | 'domain'\n\nexport interface OutreachRecipientCountry {\n /** ISO-3166-1 alpha-2, uppercase, or `null` when nothing names one. */\n country: string | null\n source: OutreachCountrySource | null\n}\n\n/**\n * Country-code top-level domains read as their country: the ones registries\n * reserve for, or that are overwhelmingly used by, people and companies in\n * that country. `.uk` is Great Britain's `GB`.\n */\nconst COUNTRY_CODE_DOMAINS: Readonly<Record<string, string>> = {\n us: 'US',\n ca: 'CA',\n mx: 'MX',\n br: 'BR',\n ar: 'AR',\n cl: 'CL',\n uk: 'GB',\n ie: 'IE',\n de: 'DE',\n at: 'AT',\n ch: 'CH',\n fr: 'FR',\n be: 'BE',\n nl: 'NL',\n lu: 'LU',\n es: 'ES',\n pt: 'PT',\n it: 'IT',\n gr: 'GR',\n se: 'SE',\n no: 'NO',\n dk: 'DK',\n fi: 'FI',\n pl: 'PL',\n cz: 'CZ',\n sk: 'SK',\n hu: 'HU',\n ro: 'RO',\n bg: 'BG',\n hr: 'HR',\n si: 'SI',\n ee: 'EE',\n lt: 'LT',\n lv: 'LV',\n mt: 'MT',\n cy: 'CY',\n ru: 'RU',\n ua: 'UA',\n tr: 'TR',\n il: 'IL',\n ae: 'AE',\n za: 'ZA',\n in: 'IN',\n cn: 'CN',\n jp: 'JP',\n kr: 'KR',\n sg: 'SG',\n hk: 'HK',\n au: 'AU',\n nz: 'NZ',\n}\n\n/** The country an address's domain names, or `null` for a generic one. */\nexport function countryFromEmailDomain(email: unknown): string | null {\n const value = String(email ?? '').trim().toLowerCase()\n const domain = value.slice(value.lastIndexOf('@') + 1).replace(/\\.+$/, '')\n const tld = domain.slice(domain.lastIndexOf('.') + 1)\n if (!domain.includes('.') || !tld) return null\n return COUNTRY_CODE_DOMAINS[tld] ?? null\n}\n\nfunction addressCountry(address: unknown): string | null {\n if (!address || typeof address !== 'object') return null\n return normalizeAddress(address as AglynPostalAddress)?.country ?? null\n}\n\n/**\n * The recipient's country from the best source that names one — see the\n * module note for the order. `contactAddress` is the address on the sending\n * group's facet of the contact; `companyAddress` is the one on the company\n * the contact is filed under.\n */\nexport function resolveOutreachRecipientCountry(input: {\n email: string\n contactAddress?: unknown\n companyAddress?: unknown\n}): OutreachRecipientCountry {\n const fromContact = addressCountry(input.contactAddress)\n if (fromContact) return { country: fromContact, source: 'contact' }\n const fromCompany = addressCountry(input.companyAddress)\n if (fromCompany) return { country: fromCompany, source: 'company' }\n const fromDomain = countryFromEmailDomain(input.email)\n if (fromDomain) return { country: fromDomain, source: 'domain' }\n return { country: null, source: null }\n}\n\n/** Countries whose English name is read with \"the\": \"the United States\". */\nconst DEFINITE_ARTICLE = new Set(['US', 'GB', 'NL', 'AE', 'PH', 'DO', 'BS', 'GM'])\n\nlet regionNames: Intl.DisplayNames | null | undefined\n\n/**\n * A country's English name as a sentence says it — \"the United States\",\n * \"Canada\" — or the code itself where the runtime has no region names.\n */\nexport function outreachCountryName(code: string): string {\n const upper = String(code ?? '').toUpperCase()\n if (regionNames === undefined) {\n try {\n regionNames = new Intl.DisplayNames(['en'], { type: 'region' })\n } catch {\n regionNames = null\n }\n }\n let name: string\n try {\n name = regionNames?.of(upper) ?? upper\n } catch {\n // A code `Intl` rejects outright is printed as it came.\n name = upper\n }\n return DEFINITE_ARTICLE.has(upper) && name !== upper ? `the ${name}` : name\n}\n"],"names":["normalizeAddress","COUNTRY_CODE_DOMAINS","us","ca","mx","br","ar","cl","uk","ie","de","at","ch","fr","be","nl","lu","es","pt","it","gr","se","no","dk","fi","pl","cz","sk","hu","ro","bg","hr","si","ee","lt","lv","mt","cy","ru","ua","tr","il","ae","za","in","cn","jp","kr","sg","hk","au","nz","countryFromEmailDomain","email","value","String","trim","toLowerCase","domain","slice","lastIndexOf","replace","tld","includes","addressCountry","address","country","resolveOutreachRecipientCountry","input","fromContact","contactAddress","source","fromCompany","companyAddress","fromDomain","DEFINITE_ARTICLE","Set","regionNames","outreachCountryName","code","upper","toUpperCase","undefined","Intl","DisplayNames","type","name","of","has"],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
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 WeeklySchedule } from '@aglyn/shared-util-timestamp/zoned-time';
|
|
18
|
+
import type { OutreachSendWindow } from '../model/outreach.types';
|
|
19
|
+
/** How far a due time may move, in total: half of it either way. */
|
|
20
|
+
export declare const OUTREACH_DUE_JITTER_MINUTES = 60;
|
|
21
|
+
/**
|
|
22
|
+
* How long an out-of-office reply holds the next step back, in business
|
|
23
|
+
* days — a working week, the usual length of an absence — measured from
|
|
24
|
+
* when the reply arrived. A step already due later than that keeps its time.
|
|
25
|
+
*/
|
|
26
|
+
export declare const OUTREACH_AUTO_REPLY_POSTPONE_BUSINESS_DAYS = 5;
|
|
27
|
+
/** A source of numbers in `[0, 1)`, such as `Math.random`. */
|
|
28
|
+
export type OutreachRandom = () => number;
|
|
29
|
+
/** A send window as the weekly schedule the zone math reads. */
|
|
30
|
+
export declare function outreachWindowSchedule(window: OutreachSendWindow | null | undefined): WeeklySchedule;
|
|
31
|
+
/** The window a sequence's steps go out in: its own, or else its mailbox's. */
|
|
32
|
+
export declare function effectiveOutreachWindow(sequenceWindow: OutreachSendWindow | null | undefined, mailboxWindow: OutreachSendWindow | null | undefined): OutreachSendWindow | null;
|
|
33
|
+
/** Whether `atMs` is inside the window, read in `timeZone`. */
|
|
34
|
+
export declare function isOutreachWindowOpen(atMs: number, window: OutreachSendWindow | null | undefined, timeZone: string): boolean;
|
|
35
|
+
export interface OutreachDueInput {
|
|
36
|
+
/** The instant the wait is counted from: the last step's send, or the enrollment. */
|
|
37
|
+
fromMs: number;
|
|
38
|
+
delayBusinessDays: number;
|
|
39
|
+
/** The mailbox's IANA zone. */
|
|
40
|
+
timeZone: string;
|
|
41
|
+
/** The window the step goes out in — {@link effectiveOutreachWindow}. */
|
|
42
|
+
window: OutreachSendWindow | null | undefined;
|
|
43
|
+
random: OutreachRandom;
|
|
44
|
+
/** Defaults to {@link OUTREACH_DUE_JITTER_MINUTES}; `0` places the time exactly. */
|
|
45
|
+
jitterMinutes?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* When a step comes due — see the module note — or `null` when it never
|
|
49
|
+
* can: a zone `Intl` does not know, a delay that is not a whole number of
|
|
50
|
+
* days, or a window that never opens.
|
|
51
|
+
*/
|
|
52
|
+
export declare function scheduleOutreachDue(input: OutreachDueInput): number | null;
|
|
53
|
+
export interface OutreachAutoReplyPostponeInput {
|
|
54
|
+
/** The enrollment's `nextDueAtMs`; `null` has nothing waiting to hold back. */
|
|
55
|
+
nextDueAtMs: number | null;
|
|
56
|
+
/** When the automatic reply arrived. */
|
|
57
|
+
receivedAtMs: number;
|
|
58
|
+
timeZone: string;
|
|
59
|
+
window: OutreachSendWindow | null | undefined;
|
|
60
|
+
random: OutreachRandom;
|
|
61
|
+
businessDays?: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The due time after an out-of-office reply: at least
|
|
65
|
+
* {@link OUTREACH_AUTO_REPLY_POSTPONE_BUSINESS_DAYS} business days after the
|
|
66
|
+
* reply, never earlier than the step was already due. An automatic reply
|
|
67
|
+
* says the person is away, not that they answered, so the enrollment waits
|
|
68
|
+
* and does not stop.
|
|
69
|
+
*/
|
|
70
|
+
export declare function postponeOutreachForAutoReply(input: OutreachAutoReplyPostponeInput): number | null;
|