@aglyn/plugins-outreach 1.0.0-beta.200 → 1.0.0-beta.202
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/enrollment-actions.d.ts +33 -0
- package/src/lib/components/enrollment-actions.js +174 -0
- package/src/lib/components/enrollment-actions.js.map +1 -0
- package/src/lib/components/enrollment-detail.d.ts +21 -0
- package/src/lib/components/enrollment-detail.js +870 -0
- package/src/lib/components/enrollment-detail.js.map +1 -0
- package/src/lib/components/enrollment-filter-params.d.ts +58 -0
- package/src/lib/components/enrollment-filter-params.js +98 -0
- package/src/lib/components/enrollment-filter-params.js.map +1 -0
- package/src/lib/components/enrollments-table.d.ts +21 -0
- package/src/lib/components/enrollments-table.js +249 -210
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/outreach-console-page.js +10 -0
- package/src/lib/components/outreach-console-page.js.map +1 -1
- package/src/lib/components/sequence-detail.d.ts +2 -0
- package/src/lib/components/sequence-detail.js +70 -8
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-report-card.d.ts +17 -0
- package/src/lib/components/sequence-report-card.js +63 -15
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/components/sequences-section.d.ts +4 -1
- package/src/lib/components/sequences-section.js +12 -1
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/components/use-outreach-api.d.ts +3 -1
- package/src/lib/components/use-outreach-api.js +6 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/components/use-outreach-enrollment.d.ts +18 -0
- package/src/lib/components/use-outreach-enrollment.js +133 -0
- package/src/lib/components/use-outreach-enrollment.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +2 -0
- package/src/lib/constants/api-routes.js +1 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/gates.d.ts +4 -3
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/engine/gateway-block.d.ts +4 -8
- package/src/lib/engine/gateway-block.js +6 -26
- package/src/lib/engine/gateway-block.js.map +1 -1
- package/src/lib/engine/mail-gateway.d.ts +18 -73
- package/src/lib/engine/mail-gateway.js +32 -206
- package/src/lib/engine/mail-gateway.js.map +1 -1
- package/src/lib/model/enrollment-engagement.d.ts +26 -2
- package/src/lib/model/enrollment-engagement.js +40 -3
- package/src/lib/model/enrollment-engagement.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +40 -0
- package/src/lib/model/enrollment-history.js +95 -0
- package/src/lib/model/enrollment-history.js.map +1 -0
- package/src/lib/model/enrollment-list-query.d.ts +6 -0
- package/src/lib/model/enrollment-list-query.js +35 -1
- package/src/lib/model/enrollment-list-query.js.map +1 -1
- package/src/lib/model/enrollment-timeline.d.ts +78 -0
- package/src/lib/model/enrollment-timeline.js +430 -0
- package/src/lib/model/enrollment-timeline.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +24 -0
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +78 -29
- package/src/lib/model/outreach.types.js +33 -8
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/routes/enroll-routes.js +26 -3
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/enrollment-routes.d.ts +10 -0
- package/src/lib/routes/enrollment-routes.js +82 -1
- package/src/lib/routes/enrollment-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +14 -4
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-deps.d.ts +3 -1
- package/src/lib/routes/route-deps.js.map +1 -1
- package/src/lib/runtime/click-events.d.ts +10 -5
- package/src/lib/runtime/click-events.js +45 -6
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/gateway-ledger.d.ts +6 -0
- package/src/lib/runtime/gateway-ledger.js +9 -3
- package/src/lib/runtime/gateway-ledger.js.map +1 -1
- package/src/lib/runtime/host-suspension.d.ts +30 -0
- package/src/lib/runtime/host-suspension.js +35 -0
- package/src/lib/runtime/host-suspension.js.map +1 -0
- package/src/lib/runtime/person-erasure.d.ts +4 -1
- package/src/lib/runtime/person-erasure.js +14 -3
- package/src/lib/runtime/person-erasure.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.js +34 -3
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +27 -1
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +46 -6
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +4 -0
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/domain-intel-store.d.ts +61 -43
- package/src/lib/storage/domain-intel-store.js +138 -226
- package/src/lib/storage/domain-intel-store.js.map +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -14,64 +15,103 @@
|
|
|
14
15
|
* See the License for the specific language governing permissions and
|
|
15
16
|
* limitations under the License.
|
|
16
17
|
*/ /**
|
|
17
|
-
* WHAT
|
|
18
|
+
* WHAT SEQUENCES KNOWS ABOUT THE DOMAINS IT WRITES TO (AGL-3326).
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
+
* Both halves live on the platform (AGL-3328), in `@aglyn/tenant-data-admin`'s
|
|
21
|
+
* `email-deliverability`, which the Resend path reads and writes too:
|
|
20
22
|
*
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* - `
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* - a recipient domain's MX — the platform-wide `mailDomains/{domain}`
|
|
24
|
+
* cache, looked up with the resolver the caller hands in and trusted for
|
|
25
|
+
* a week (a day for a domain that takes no mail). The first workspace to
|
|
26
|
+
* write to a domain warms it for every other;
|
|
27
|
+
* - the ledger — `orgs/{orgId}/mailGatewayLedger/{sendingDomain}~{gateway}`,
|
|
28
|
+
* keyed by the MAILBOX's sending domain, because a gateway refuses a
|
|
29
|
+
* sender: the Barracuda that refused `aglyn.io` refuses `aglyn.io`, not
|
|
30
|
+
* the organization's other mailbox on another domain. It stays under the
|
|
31
|
+
* organization because a mailbox's domain is not one organization's — two
|
|
32
|
+
* workspaces can each connect a `gmail.com` mailbox.
|
|
28
33
|
*
|
|
29
34
|
* A read that could not be made answers `null` for the addresses it
|
|
30
35
|
* covered, which the gates refuse as "we couldn't check" — except that a
|
|
31
|
-
* stale answer
|
|
32
|
-
*
|
|
33
|
-
* timing out says nothing about the domain. A domain that resolves to no
|
|
34
|
-
* MX at all is `none`, and the gates block it.
|
|
36
|
+
* stale answer that the domain takes mail outranks no answer. A domain that
|
|
37
|
+
* resolves to no MX is `none`, and the gates block it.
|
|
35
38
|
*
|
|
36
|
-
* The
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
* ## The two collections this module wrote first
|
|
40
|
+
*
|
|
41
|
+
* `outreachDomainIntel` and `outreachGatewayStats` are READ THROUGH, not
|
|
42
|
+
* migrated. The first held MX answers, which a lookup re-derives, so it is
|
|
43
|
+
* no longer read. The second held a per-gateway ledger with no sending
|
|
44
|
+
* domain; its days are summed into every sending domain's standing for the
|
|
45
|
+
* thirty days they can still hold a send — the conservative reading, since
|
|
46
|
+
* an organization with one mailbox domain loses nothing and one with two
|
|
47
|
+
* holds a little more. Nothing writes either any more; both are retired,
|
|
48
|
+
* with their rules, once the window has passed.
|
|
49
|
+
*/ import { mailGatewayStanding, normalizeLedgerSendingDomain, resolveMailDomain } from "@aglyn/shared-util-email";
|
|
50
|
+
import { readMailDomains, readMailGatewayLedgers, recordMailGatewayOutcome } from "@aglyn/tenant-data-admin/server/email-deliverability";
|
|
51
|
+
import { OUTREACH_MAIL_GATEWAYS } from "../engine/mail-gateway.js";
|
|
52
|
+
import { outreachEmailDomain } from "../engine/do-not-contact-domain.js";
|
|
43
53
|
import { outreachOrgCollection } from "./outreach-records.js";
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
/** The resolver the platform store reads with, from what a caller hands in. */ function resolverOf(input) {
|
|
55
|
+
return _extends({
|
|
56
|
+
resolveMx: input.resolveMx
|
|
57
|
+
}, input.resolveAddress ? {
|
|
58
|
+
resolveAddress: input.resolveAddress
|
|
59
|
+
} : {});
|
|
60
|
+
}
|
|
61
|
+
/** The domain a mailbox's mail leaves from: its send-as address's, else the account's. */ export function outreachMailboxSendingDomain(mailbox) {
|
|
62
|
+
return normalizeLedgerSendingDomain((mailbox == null ? void 0 : mailbox.sendAs) || (mailbox == null ? void 0 : mailbox.email));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A domain's MX, resolved and classified. A domain the resolver says has no
|
|
66
|
+
* MX answers `none`; a resolver that could not answer rejects, and the
|
|
67
|
+
* caller decides what a missing answer means.
|
|
68
|
+
*/ export async function resolveOutreachDomainMx(resolveMx, domain) {
|
|
69
|
+
const intel = await resolveMailDomain({
|
|
70
|
+
resolveMx
|
|
71
|
+
}, domain, 0);
|
|
72
|
+
return {
|
|
73
|
+
mx: intel.mx,
|
|
74
|
+
gateway: intel.gateway
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The MX answer for every address, keyed by address — read through the
|
|
79
|
+
* platform cache, and looked up and stored when missing or stale. An
|
|
80
|
+
* address with no domain, and one whose domain could not be looked up and
|
|
81
|
+
* was never known, answers `null`.
|
|
82
|
+
*/ export async function readOutreachDomainIntel(firestore, _orgId, emails, input) {
|
|
83
|
+
var _intel_get;
|
|
84
|
+
const answers = new Map();
|
|
85
|
+
const domainOf = new Map();
|
|
86
|
+
for (const email of emails){
|
|
87
|
+
const domain = outreachEmailDomain(email);
|
|
88
|
+
if (domain) domainOf.set(email, domain);
|
|
89
|
+
else answers.set(email, null);
|
|
90
|
+
}
|
|
91
|
+
if (!domainOf.size) return answers;
|
|
92
|
+
let intel;
|
|
93
|
+
try {
|
|
94
|
+
intel = await readMailDomains([
|
|
95
|
+
...new Set(domainOf.values())
|
|
96
|
+
], {
|
|
97
|
+
firestore,
|
|
98
|
+
resolver: resolverOf(input),
|
|
99
|
+
nowMs: input.nowMs
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error('[outreach] domain intel lookup failed; reading as unchecked', error);
|
|
103
|
+
intel = new Map();
|
|
104
|
+
}
|
|
105
|
+
for (const [email, domain] of domainOf)answers.set(email, (_intel_get = intel.get(domain)) != null ? _intel_get : null);
|
|
106
|
+
return answers;
|
|
107
|
+
}
|
|
51
108
|
const count = (value)=>{
|
|
52
109
|
const number = Number(value);
|
|
53
110
|
return Number.isFinite(number) ? Math.max(0, Math.floor(number)) : 0;
|
|
54
111
|
};
|
|
55
112
|
const ms = (value)=>typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
56
113
|
const gatewayOf = (value)=>OUTREACH_MAIL_GATEWAYS.includes(value) ? value : null;
|
|
57
|
-
/** A stored
|
|
58
|
-
var _ms, _ms1;
|
|
59
|
-
if (!data) return null;
|
|
60
|
-
const gateway = gatewayOf(data['gateway']);
|
|
61
|
-
if (!gateway) return null;
|
|
62
|
-
return {
|
|
63
|
-
domain: typeof data['domain'] === 'string' && data['domain'] ? data['domain'] : id,
|
|
64
|
-
mx: Array.isArray(data['mx']) ? data['mx'].filter((entry)=>typeof entry === 'string') : [],
|
|
65
|
-
gateway,
|
|
66
|
-
resolvedAtMs: (_ms = ms(data['resolvedAtMs'])) != null ? _ms : 0,
|
|
67
|
-
sent: count(data['sent']),
|
|
68
|
-
delivered: count(data['delivered']),
|
|
69
|
-
blocked: count(data['blocked']),
|
|
70
|
-
lastBlockedAtMs: ms(data['lastBlockedAtMs']),
|
|
71
|
-
updatedAtMs: (_ms1 = ms(data['updatedAtMs'])) != null ? _ms1 : 0
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
/** A stored gateway ledger in its model shape, or `null` for no document. */ export function readStoredOutreachGatewayStats(id, data) {
|
|
114
|
+
/** A stored per-gateway ledger from before the sending domain keyed it, or `null` for no document. */ export function readStoredOutreachGatewayStats(id, data) {
|
|
75
115
|
var _gatewayOf, _ms;
|
|
76
116
|
if (!data) return null;
|
|
77
117
|
const gateway = (_gatewayOf = gatewayOf(data['gateway'])) != null ? _gatewayOf : gatewayOf(id);
|
|
@@ -98,118 +138,13 @@ const gatewayOf = (value)=>OUTREACH_MAIL_GATEWAYS.includes(value) ? value : null
|
|
|
98
138
|
updatedAtMs: (_ms = ms(data['updatedAtMs'])) != null ? _ms : 0
|
|
99
139
|
};
|
|
100
140
|
}
|
|
101
|
-
/**
|
|
102
|
-
* A domain's MX, resolved and classified — see the module note. A domain
|
|
103
|
-
* the resolver says has no MX answers `none`; a resolver that could not
|
|
104
|
-
* answer rejects, and the caller decides what a missing answer means.
|
|
105
|
-
*/ export async function resolveOutreachDomainMx(resolveMx, domain) {
|
|
106
|
-
let records;
|
|
107
|
-
try {
|
|
108
|
-
records = await resolveMx(domain);
|
|
109
|
-
} catch (error) {
|
|
110
|
-
var _ref;
|
|
111
|
-
const code = String((_ref = error == null ? void 0 : error.code) != null ? _ref : '').toUpperCase();
|
|
112
|
-
if (NO_MX_CODES.has(code)) return {
|
|
113
|
-
mx: [],
|
|
114
|
-
gateway: 'none'
|
|
115
|
-
};
|
|
116
|
-
throw error;
|
|
117
|
-
}
|
|
118
|
-
const mx = [
|
|
119
|
-
...records
|
|
120
|
-
].filter((record)=>record && typeof record.exchange === 'string').sort((a, b)=>(Number(a.priority) || 0) - (Number(b.priority) || 0)).map((record)=>record.exchange.trim().toLowerCase().replace(/\.$/, ''))// RFC 7505's null MX — a lone "." — says the domain takes no mail.
|
|
121
|
-
.filter((exchange)=>exchange && exchange !== '.');
|
|
122
|
-
return {
|
|
123
|
-
mx,
|
|
124
|
-
gateway: classifyOutreachMailGateway(mx)
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
const intelCollection = (firestore, orgId)=>outreachOrgCollection(firestore, orgId, 'domainIntel');
|
|
128
|
-
const statsCollection = (firestore, orgId)=>outreachOrgCollection(firestore, orgId, 'gatewayStats');
|
|
129
|
-
/**
|
|
130
|
-
* The domain intel for every address, keyed by address — fresh from the
|
|
131
|
-
* store, or looked up and stored when missing or older than the TTL. An
|
|
132
|
-
* address with no domain, and one whose domain could not be looked up and
|
|
133
|
-
* was never known, answers `null`.
|
|
134
|
-
*/ export async function readOutreachDomainIntel(firestore, orgId, emails, input) {
|
|
135
|
-
var _known_get;
|
|
136
|
-
const answers = new Map();
|
|
137
|
-
const domainOf = new Map();
|
|
138
|
-
for (const email of emails){
|
|
139
|
-
const domain = outreachEmailDomain(email);
|
|
140
|
-
if (domain) domainOf.set(email, domain);
|
|
141
|
-
else answers.set(email, null);
|
|
142
|
-
}
|
|
143
|
-
const domains = [
|
|
144
|
-
...new Set(domainOf.values())
|
|
145
|
-
];
|
|
146
|
-
if (!domains.length) return answers;
|
|
147
|
-
const collection = intelCollection(firestore, orgId);
|
|
148
|
-
const known = new Map();
|
|
149
|
-
try {
|
|
150
|
-
for(let start = 0; start < domains.length; start += GET_ALL_CHUNK){
|
|
151
|
-
const chunk = domains.slice(start, start + GET_ALL_CHUNK);
|
|
152
|
-
const snapshots = await firestore.getAll(...chunk.map((domain)=>collection.doc(domain)));
|
|
153
|
-
chunk.forEach((domain, index)=>{
|
|
154
|
-
const snapshot = snapshots[index];
|
|
155
|
-
known.set(domain, readStoredOutreachDomainIntel(domain, (snapshot == null ? void 0 : snapshot.exists) ? snapshot.data() : undefined));
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
} catch (error) {
|
|
159
|
-
console.error('[outreach] domain intel lookup failed; reading as unchecked', error);
|
|
160
|
-
for (const email of domainOf.keys())answers.set(email, null);
|
|
161
|
-
return answers;
|
|
162
|
-
}
|
|
163
|
-
await Promise.all(domains.map(async (domain)=>{
|
|
164
|
-
var _known_get, _ref, _ref1, _ref2, _ref3;
|
|
165
|
-
const stored = (_known_get = known.get(domain)) != null ? _known_get : null;
|
|
166
|
-
if (stored && stored.resolvedAtMs + OUTREACH_DOMAIN_INTEL_TTL_MS > input.nowMs) return;
|
|
167
|
-
let resolved;
|
|
168
|
-
try {
|
|
169
|
-
resolved = await resolveOutreachDomainMx(input.resolveMx, domain);
|
|
170
|
-
} catch (error) {
|
|
171
|
-
// A resolver that could not answer: the stale answer stands, and a
|
|
172
|
-
// domain never looked up is unchecked.
|
|
173
|
-
console.error(`[outreach] the MX of ${domain} could not be looked up`, error);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const fresh = {
|
|
177
|
-
domain,
|
|
178
|
-
mx: resolved.mx,
|
|
179
|
-
gateway: resolved.gateway,
|
|
180
|
-
resolvedAtMs: input.nowMs,
|
|
181
|
-
sent: (_ref = stored == null ? void 0 : stored.sent) != null ? _ref : 0,
|
|
182
|
-
delivered: (_ref1 = stored == null ? void 0 : stored.delivered) != null ? _ref1 : 0,
|
|
183
|
-
blocked: (_ref2 = stored == null ? void 0 : stored.blocked) != null ? _ref2 : 0,
|
|
184
|
-
lastBlockedAtMs: (_ref3 = stored == null ? void 0 : stored.lastBlockedAtMs) != null ? _ref3 : null,
|
|
185
|
-
updatedAtMs: input.nowMs
|
|
186
|
-
};
|
|
187
|
-
known.set(domain, fresh);
|
|
188
|
-
try {
|
|
189
|
-
await collection.doc(domain).set({
|
|
190
|
-
domain,
|
|
191
|
-
mx: fresh.mx,
|
|
192
|
-
gateway: fresh.gateway,
|
|
193
|
-
resolvedAtMs: fresh.resolvedAtMs,
|
|
194
|
-
updatedAtMs: fresh.updatedAtMs
|
|
195
|
-
}, {
|
|
196
|
-
merge: true
|
|
197
|
-
});
|
|
198
|
-
} catch (error) {
|
|
199
|
-
// The cache is a convenience; the answer is already in hand.
|
|
200
|
-
console.error(`[outreach] the MX of ${domain} could not be cached`, error);
|
|
201
|
-
}
|
|
202
|
-
}));
|
|
203
|
-
for (const [email, domain] of domainOf)answers.set(email, (_known_get = known.get(domain)) != null ? _known_get : null);
|
|
204
|
-
return answers;
|
|
205
|
-
}
|
|
206
|
-
/** The ledger of each gateway named, keyed by gateway; a gateway with no ledger yet answers `null`. */ export async function readOutreachGatewayStats(firestore, orgId, gateways) {
|
|
141
|
+
/** The per-gateway ledgers written before the sending domain keyed them — see the module note. */ async function readLegacyGatewayStats(firestore, orgId, gateways) {
|
|
207
142
|
const answers = new Map();
|
|
208
143
|
const wanted = [
|
|
209
144
|
...new Set(gateways)
|
|
210
|
-
];
|
|
145
|
+
].filter((gateway)=>gateway !== 'none');
|
|
211
146
|
if (!wanted.length) return answers;
|
|
212
|
-
const collection =
|
|
147
|
+
const collection = outreachOrgCollection(firestore, orgId, 'gatewayStats');
|
|
213
148
|
const snapshots = await firestore.getAll(...wanted.map((gateway)=>collection.doc(gateway)));
|
|
214
149
|
wanted.forEach((gateway, index)=>{
|
|
215
150
|
const snapshot = snapshots[index];
|
|
@@ -217,29 +152,39 @@ const statsCollection = (firestore, orgId)=>outreachOrgCollection(firestore, org
|
|
|
217
152
|
});
|
|
218
153
|
return answers;
|
|
219
154
|
}
|
|
220
|
-
/**
|
|
221
|
-
const week = outreachGatewayWindow(stats == null ? void 0 : stats.days, nowMs, OUTREACH_GATEWAY_CHIP_DAYS);
|
|
222
|
-
const month = outreachGatewayWindow(stats == null ? void 0 : stats.days, nowMs, OUTREACH_GATEWAY_WINDOW_DAYS);
|
|
155
|
+
/** Two standings for one gateway, summed. */ function sumStandings(a, b) {
|
|
223
156
|
return {
|
|
224
|
-
gateway:
|
|
225
|
-
blocked7:
|
|
226
|
-
delivered7:
|
|
227
|
-
blocked30:
|
|
228
|
-
delivered30:
|
|
157
|
+
gateway: a.gateway,
|
|
158
|
+
blocked7: a.blocked7 + b.blocked7,
|
|
159
|
+
delivered7: a.delivered7 + b.delivered7,
|
|
160
|
+
blocked30: a.blocked30 + b.blocked30,
|
|
161
|
+
delivered30: a.delivered30 + b.delivered30
|
|
229
162
|
};
|
|
230
163
|
}
|
|
231
164
|
/**
|
|
232
|
-
* The gateway standing of every address, keyed by address: the
|
|
233
|
-
*
|
|
234
|
-
*
|
|
165
|
+
* The gateway standing of every address, keyed by address: the MX answer
|
|
166
|
+
* read (and refreshed) above, joined to the mailbox's sending-domain ledger
|
|
167
|
+
* with each gateway it names, plus what the legacy per-gateway ledger still
|
|
168
|
+
* holds. `null` for an address whose domain could not be checked.
|
|
235
169
|
*/ export async function readOutreachGatewayStandings(firestore, orgId, emails, input) {
|
|
236
170
|
const intel = await readOutreachDomainIntel(firestore, orgId, emails, input);
|
|
237
171
|
const gateways = [
|
|
238
172
|
...intel.values()
|
|
239
173
|
].filter((entry)=>entry !== null).map((entry)=>entry.gateway);
|
|
240
|
-
|
|
174
|
+
const sendingDomain = normalizeLedgerSendingDomain(input.sendingDomain);
|
|
175
|
+
let ledgers;
|
|
176
|
+
let legacy;
|
|
241
177
|
try {
|
|
242
|
-
|
|
178
|
+
;
|
|
179
|
+
[ledgers, legacy] = await Promise.all([
|
|
180
|
+
sendingDomain ? readMailGatewayLedgers({
|
|
181
|
+
orgId
|
|
182
|
+
}, sendingDomain, gateways, {
|
|
183
|
+
firestore,
|
|
184
|
+
nowMs: input.nowMs
|
|
185
|
+
}) : Promise.resolve(new Map()),
|
|
186
|
+
readLegacyGatewayStats(firestore, orgId, gateways)
|
|
187
|
+
]);
|
|
243
188
|
} catch (error) {
|
|
244
189
|
console.error('[outreach] gateway ledger lookup failed; reading as unchecked', error);
|
|
245
190
|
return new Map(emails.map((email)=>[
|
|
@@ -249,72 +194,39 @@ const statsCollection = (firestore, orgId)=>outreachOrgCollection(firestore, org
|
|
|
249
194
|
}
|
|
250
195
|
const answers = new Map();
|
|
251
196
|
for (const email of emails){
|
|
252
|
-
var _intel_get,
|
|
197
|
+
var _intel_get, _ledgers_get, _legacy_get;
|
|
253
198
|
const entry = (_intel_get = intel.get(email)) != null ? _intel_get : null;
|
|
254
|
-
|
|
199
|
+
if (!entry) {
|
|
200
|
+
answers.set(email, null);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const current = mailGatewayStanding(entry.gateway, (_ledgers_get = ledgers.get(entry.gateway)) != null ? _ledgers_get : null, input.nowMs);
|
|
204
|
+
const previous = mailGatewayStanding(entry.gateway, (_legacy_get = legacy.get(entry.gateway)) != null ? _legacy_get : null, input.nowMs);
|
|
205
|
+
answers.set(email, sumStandings(current, previous));
|
|
255
206
|
}
|
|
256
207
|
return answers;
|
|
257
208
|
}
|
|
258
209
|
/**
|
|
259
|
-
* Counts an outcome on the
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
210
|
+
* Counts an outcome on the mailbox's sending-domain ledger with a gateway.
|
|
211
|
+
* Never throws: the send or the bounce it records has already happened,
|
|
212
|
+
* and a lost count understates a gateway while a thrown one loses the step
|
|
213
|
+
* that called. A mailbox whose sending domain cannot be read counts nothing.
|
|
263
214
|
*/ export async function recordOutreachGatewayOutcome(firestore, orgId, input) {
|
|
264
|
-
var
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const stats = readStoredOutreachGatewayStats(input.gateway, statsSnapshot.exists ? statsSnapshot.data() : undefined);
|
|
280
|
-
const blockedAt = input.outcome === 'blocked' ? input.atMs : null;
|
|
281
|
-
const nextIntel = {
|
|
282
|
-
domain,
|
|
283
|
-
// A domain never looked up — enrolled before the intel existed —
|
|
284
|
-
// is written with what the bounce said, and looked up on its next read.
|
|
285
|
-
mx: (_ref = intel == null ? void 0 : intel.mx) != null ? _ref : [],
|
|
286
|
-
gateway: (_ref1 = intel == null ? void 0 : intel.gateway) != null ? _ref1 : input.gateway,
|
|
287
|
-
resolvedAtMs: (_ref2 = intel == null ? void 0 : intel.resolvedAtMs) != null ? _ref2 : 0,
|
|
288
|
-
sent: ((_ref3 = intel == null ? void 0 : intel.sent) != null ? _ref3 : 0) + (input.outcome === 'sent' ? by : 0),
|
|
289
|
-
delivered: ((_ref4 = intel == null ? void 0 : intel.delivered) != null ? _ref4 : 0) + (input.outcome === 'delivered' ? by : 0),
|
|
290
|
-
blocked: ((_ref5 = intel == null ? void 0 : intel.blocked) != null ? _ref5 : 0) + (input.outcome === 'blocked' ? by : 0),
|
|
291
|
-
lastBlockedAtMs: Math.max((_ref6 = intel == null ? void 0 : intel.lastBlockedAtMs) != null ? _ref6 : 0, blockedAt != null ? blockedAt : 0) || null,
|
|
292
|
-
updatedAtMs: input.atMs
|
|
293
|
-
};
|
|
294
|
-
const days = pruneOutreachGatewayDays(stats == null ? void 0 : stats.days, input.atMs, OUTREACH_GATEWAY_WINDOW_DAYS);
|
|
295
|
-
const today = (_days_day = days[day]) != null ? _days_day : {
|
|
296
|
-
sent: 0,
|
|
297
|
-
delivered: 0,
|
|
298
|
-
blocked: 0
|
|
299
|
-
};
|
|
300
|
-
days[day] = _extends({}, today, {
|
|
301
|
-
[input.outcome]: today[input.outcome] + by
|
|
302
|
-
});
|
|
303
|
-
const nextStats = {
|
|
304
|
-
gateway: input.gateway,
|
|
305
|
-
sent: ((_ref7 = stats == null ? void 0 : stats.sent) != null ? _ref7 : 0) + (input.outcome === 'sent' ? by : 0),
|
|
306
|
-
delivered: ((_ref8 = stats == null ? void 0 : stats.delivered) != null ? _ref8 : 0) + (input.outcome === 'delivered' ? by : 0),
|
|
307
|
-
blocked: ((_ref9 = stats == null ? void 0 : stats.blocked) != null ? _ref9 : 0) + (input.outcome === 'blocked' ? by : 0),
|
|
308
|
-
lastBlockedAtMs: Math.max((_ref10 = stats == null ? void 0 : stats.lastBlockedAtMs) != null ? _ref10 : 0, blockedAt != null ? blockedAt : 0) || null,
|
|
309
|
-
days,
|
|
310
|
-
updatedAtMs: input.atMs
|
|
311
|
-
};
|
|
312
|
-
transaction.set(intelRef, nextIntel);
|
|
313
|
-
transaction.set(statsRef, nextStats);
|
|
314
|
-
});
|
|
315
|
-
} catch (error) {
|
|
316
|
-
console.error(`[outreach] the gateway ledger could not count a ${input.outcome} at ${domain}`, error);
|
|
317
|
-
}
|
|
215
|
+
var _input_detail;
|
|
216
|
+
const sendingDomain = normalizeLedgerSendingDomain(input.sendingDomain);
|
|
217
|
+
if (!sendingDomain) return;
|
|
218
|
+
await recordMailGatewayOutcome({
|
|
219
|
+
orgId
|
|
220
|
+
}, {
|
|
221
|
+
sendingDomain,
|
|
222
|
+
gateway: input.gateway,
|
|
223
|
+
outcome: input.outcome,
|
|
224
|
+
count: input.count,
|
|
225
|
+
atMs: input.atMs,
|
|
226
|
+
detail: (_input_detail = input.detail) != null ? _input_detail : null
|
|
227
|
+
}, {
|
|
228
|
+
firestore
|
|
229
|
+
});
|
|
318
230
|
}
|
|
319
231
|
|
|
320
232
|
//# sourceMappingURL=domain-intel-store.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/storage/domain-intel-store.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 THE ORGANIZATION KNOWS ABOUT THE DOMAINS IT WRITES TO (AGL-3326).\n *\n * Two documents, both server-written:\n *\n * - `outreachDomainIntel/{domain}` — the domain's MX records and the mail\n * gateway they name, looked up with the resolver the caller hands in and\n * trusted for {@link OUTREACH_DOMAIN_INTEL_TTL_MS}; beside them, what this\n * organization's mail met at the domain;\n * - `outreachGatewayStats/{gateway}` — the same three counts per gateway,\n * in total and by UTC day, which is the ledger the hold and the\n * Check-people chip read.\n *\n * A read that could not be made answers `null` for the addresses it\n * covered, which the gates refuse as \"we couldn't check\" — except that a\n * stale answer outranks no answer: a domain whose MX was known last week\n * and cannot be looked up today is read as it was, because a resolver\n * timing out says nothing about the domain. A domain that resolves to no\n * MX at all is `none`, and the gates block it.\n *\n * The ledger is written in a transaction over both documents so a run that\n * records a block and a run that credits a delivery cannot lose each\n * other's count, and the days map is pruned on every write so it never\n * grows past the window it serves.\n */\n\nimport type { OutreachGatewayStanding, OutreachMailGateway } from '../engine/mail-gateway'\nimport {\n classifyOutreachMailGateway,\n OUTREACH_DOMAIN_INTEL_TTL_MS,\n OUTREACH_GATEWAY_CHIP_DAYS,\n OUTREACH_GATEWAY_WINDOW_DAYS,\n OUTREACH_MAIL_GATEWAYS,\n outreachGatewayDay,\n outreachGatewayWindow,\n pruneOutreachGatewayDays,\n} from '../engine/mail-gateway'\nimport { normalizeOutreachDomain, outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport type { OutreachDomainIntel, OutreachGatewayStats } from '../model/outreach.types'\nimport { outreachOrgCollection } from './outreach-records'\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** One MX record as Node's resolver answers it. */\nexport interface OutreachMxRecord {\n exchange: string\n priority: number\n}\n\n/**\n * `dns.promises.resolveMx`, or a stand-in: the platform hands in Node's,\n * the specs hand in a table. Rejects the way Node does — `code`\n * `ENOTFOUND` or `ENODATA` for a domain with no MX — and any other\n * rejection is a resolver that could not answer.\n */\nexport type OutreachResolveMx = (domain: string) => Promise<ReadonlyArray<OutreachMxRecord>>\n\n/** What a domain-intel read needs beside the store. */\nexport interface OutreachDomainIntelReadInput {\n resolveMx: OutreachResolveMx\n nowMs: number\n}\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\n/** The resolver's codes that mean \"this domain has no MX\", as opposed to \"I could not ask\". */\nconst NO_MX_CODES = new Set(['ENOTFOUND', 'ENODATA', 'NXDOMAIN', 'NOTFOUND'])\n\nconst count = (value: unknown): number => {\n const number = Number(value)\n return Number.isFinite(number) ? Math.max(0, Math.floor(number)) : 0\n}\nconst ms = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null\nconst gatewayOf = (value: unknown): OutreachMailGateway | null =>\n (OUTREACH_MAIL_GATEWAYS as readonly unknown[]).includes(value) ? (value as OutreachMailGateway) : null\n\n/** A stored domain-intel document in its model shape, or `null` for no document. */\nexport function readStoredOutreachDomainIntel(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachDomainIntel | null {\n if (!data) return null\n const gateway = gatewayOf(data['gateway'])\n if (!gateway) return null\n return {\n domain: typeof data['domain'] === 'string' && data['domain'] ? data['domain'] : id,\n mx: Array.isArray(data['mx']) ? data['mx'].filter((entry): entry is string => typeof entry === 'string') : [],\n gateway,\n resolvedAtMs: ms(data['resolvedAtMs']) ?? 0,\n sent: count(data['sent']),\n delivered: count(data['delivered']),\n blocked: count(data['blocked']),\n lastBlockedAtMs: ms(data['lastBlockedAtMs']),\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/** A stored gateway ledger in its model shape, or `null` for no document. */\nexport function readStoredOutreachGatewayStats(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachGatewayStats | null {\n if (!data) return null\n const gateway = gatewayOf(data['gateway']) ?? gatewayOf(id)\n if (!gateway) return null\n const days: OutreachGatewayStats['days'] = {}\n const raw = data['days']\n if (raw && typeof raw === 'object') {\n for (const [day, counts] of Object.entries(raw as Record<string, unknown>)) {\n const entry = (counts && typeof counts === 'object' ? counts : {}) as Record<string, unknown>\n days[day] = { sent: count(entry['sent']), delivered: count(entry['delivered']), blocked: count(entry['blocked']) }\n }\n }\n return {\n gateway,\n sent: count(data['sent']),\n delivered: count(data['delivered']),\n blocked: count(data['blocked']),\n lastBlockedAtMs: ms(data['lastBlockedAtMs']),\n days,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/**\n * A domain's MX, resolved and classified — see the module note. A domain\n * the resolver says has no MX answers `none`; a resolver that could not\n * answer rejects, and the caller decides what a missing answer means.\n */\nexport async function resolveOutreachDomainMx(\n resolveMx: OutreachResolveMx,\n domain: string,\n): Promise<{ mx: string[]; gateway: OutreachMailGateway }> {\n let records: ReadonlyArray<OutreachMxRecord>\n try {\n records = await resolveMx(domain)\n } catch (error) {\n const code = String((error as { code?: unknown })?.code ?? '').toUpperCase()\n if (NO_MX_CODES.has(code)) return { mx: [], gateway: 'none' }\n throw error\n }\n const mx = [...records]\n .filter((record) => record && typeof record.exchange === 'string')\n .sort((a, b) => (Number(a.priority) || 0) - (Number(b.priority) || 0))\n .map((record) => record.exchange.trim().toLowerCase().replace(/\\.$/, ''))\n // RFC 7505's null MX — a lone \".\" — says the domain takes no mail.\n .filter((exchange) => exchange && exchange !== '.')\n return { mx, gateway: classifyOutreachMailGateway(mx) }\n}\n\nconst intelCollection = (firestore: Firestore, orgId: string) =>\n outreachOrgCollection(firestore, orgId, 'domainIntel')\nconst statsCollection = (firestore: Firestore, orgId: string) =>\n outreachOrgCollection(firestore, orgId, 'gatewayStats')\n\n/**\n * The domain intel for every address, keyed by address — fresh from the\n * store, or looked up and stored when missing or older than the TTL. An\n * address with no domain, and one whose domain could not be looked up and\n * was never known, answers `null`.\n */\nexport async function readOutreachDomainIntel(\n firestore: Firestore,\n orgId: string,\n emails: readonly string[],\n input: OutreachDomainIntelReadInput,\n): Promise<Map<string, OutreachDomainIntel | null>> {\n const answers = new Map<string, OutreachDomainIntel | null>()\n const domainOf = new Map<string, string>()\n for (const email of emails) {\n const domain = outreachEmailDomain(email)\n if (domain) domainOf.set(email, domain)\n else answers.set(email, null)\n }\n const domains = [...new Set(domainOf.values())]\n if (!domains.length) return answers\n const collection = intelCollection(firestore, orgId)\n const known = new Map<string, OutreachDomainIntel | null>()\n try {\n for (let start = 0; start < domains.length; start += GET_ALL_CHUNK) {\n const chunk = domains.slice(start, start + GET_ALL_CHUNK)\n const snapshots = await firestore.getAll(...chunk.map((domain) => collection.doc(domain)))\n chunk.forEach((domain, index) => {\n const snapshot = snapshots[index]\n known.set(domain, readStoredOutreachDomainIntel(domain, snapshot?.exists ? snapshot.data() : undefined))\n })\n }\n } catch (error) {\n console.error('[outreach] domain intel lookup failed; reading as unchecked', error)\n for (const email of domainOf.keys()) answers.set(email, null)\n return answers\n }\n await Promise.all(\n domains.map(async (domain) => {\n const stored = known.get(domain) ?? null\n if (stored && stored.resolvedAtMs + OUTREACH_DOMAIN_INTEL_TTL_MS > input.nowMs) return\n let resolved: { mx: string[]; gateway: OutreachMailGateway }\n try {\n resolved = await resolveOutreachDomainMx(input.resolveMx, domain)\n } catch (error) {\n // A resolver that could not answer: the stale answer stands, and a\n // domain never looked up is unchecked.\n console.error(`[outreach] the MX of ${domain} could not be looked up`, error)\n return\n }\n const fresh: OutreachDomainIntel = {\n domain,\n mx: resolved.mx,\n gateway: resolved.gateway,\n resolvedAtMs: input.nowMs,\n sent: stored?.sent ?? 0,\n delivered: stored?.delivered ?? 0,\n blocked: stored?.blocked ?? 0,\n lastBlockedAtMs: stored?.lastBlockedAtMs ?? null,\n updatedAtMs: input.nowMs,\n }\n known.set(domain, fresh)\n try {\n await collection.doc(domain).set(\n { domain, mx: fresh.mx, gateway: fresh.gateway, resolvedAtMs: fresh.resolvedAtMs, updatedAtMs: fresh.updatedAtMs },\n { merge: true },\n )\n } catch (error) {\n // The cache is a convenience; the answer is already in hand.\n console.error(`[outreach] the MX of ${domain} could not be cached`, error)\n }\n }),\n )\n for (const [email, domain] of domainOf) answers.set(email, known.get(domain) ?? null)\n return answers\n}\n\n/** The ledger of each gateway named, keyed by gateway; a gateway with no ledger yet answers `null`. */\nexport async function readOutreachGatewayStats(\n firestore: Firestore,\n orgId: string,\n gateways: readonly OutreachMailGateway[],\n): Promise<Map<OutreachMailGateway, OutreachGatewayStats | null>> {\n const answers = new Map<OutreachMailGateway, OutreachGatewayStats | null>()\n const wanted = [...new Set(gateways)]\n if (!wanted.length) return answers\n const collection = statsCollection(firestore, orgId)\n const snapshots = await firestore.getAll(...wanted.map((gateway) => collection.doc(gateway)))\n wanted.forEach((gateway, index) => {\n const snapshot = snapshots[index]\n answers.set(gateway, readStoredOutreachGatewayStats(gateway, snapshot?.exists ? snapshot.data() : undefined))\n })\n return answers\n}\n\n/** What the gates and the chip read for one domain: its gateway and that gateway's recent record here. */\nexport function outreachGatewayStandingOf(\n intel: OutreachDomainIntel,\n stats: OutreachGatewayStats | null,\n nowMs: number,\n): OutreachGatewayStanding {\n const week = outreachGatewayWindow(stats?.days, nowMs, OUTREACH_GATEWAY_CHIP_DAYS)\n const month = outreachGatewayWindow(stats?.days, nowMs, OUTREACH_GATEWAY_WINDOW_DAYS)\n return {\n gateway: intel.gateway,\n blocked7: week.blocked,\n delivered7: week.delivered,\n blocked30: month.blocked,\n delivered30: month.delivered,\n }\n}\n\n/**\n * The gateway standing of every address, keyed by address: the domain\n * intel read (and refreshed) above, joined to the ledger of each gateway it\n * names. `null` for an address whose domain could not be checked.\n */\nexport async function readOutreachGatewayStandings(\n firestore: Firestore,\n orgId: string,\n emails: readonly string[],\n input: OutreachDomainIntelReadInput,\n): Promise<Map<string, OutreachGatewayStanding | null>> {\n const intel = await readOutreachDomainIntel(firestore, orgId, emails, input)\n const gateways = [...intel.values()]\n .filter((entry): entry is OutreachDomainIntel => entry !== null)\n .map((entry) => entry.gateway)\n let stats: Map<OutreachMailGateway, OutreachGatewayStats | null>\n try {\n stats = await readOutreachGatewayStats(firestore, orgId, gateways)\n } catch (error) {\n console.error('[outreach] gateway ledger lookup failed; reading as unchecked', error)\n return new Map(emails.map((email) => [email, null]))\n }\n const answers = new Map<string, OutreachGatewayStanding | null>()\n for (const email of emails) {\n const entry = intel.get(email) ?? null\n answers.set(email, entry ? outreachGatewayStandingOf(entry, stats.get(entry.gateway) ?? null, input.nowMs) : null)\n }\n return answers\n}\n\n/** What a send met at a gateway, as the ledger counts it. */\nexport type OutreachGatewayOutcome = 'sent' | 'delivered' | 'blocked'\n\n/**\n * Counts an outcome on the domain's intel and on the gateway's ledger — see\n * the module note. Never throws: the send or the bounce it records has\n * already happened, and a lost count understates a gateway while a thrown\n * one loses the step that called.\n */\nexport async function recordOutreachGatewayOutcome(\n firestore: Firestore,\n orgId: string,\n input: { domain: string; gateway: OutreachMailGateway; outcome: OutreachGatewayOutcome; count?: number; atMs: number },\n): Promise<void> {\n const domain = normalizeOutreachDomain(input.domain)\n const by = count(input.count ?? 1)\n if (!domain || !by) return\n const intelRef = intelCollection(firestore, orgId).doc(domain)\n const statsRef = statsCollection(firestore, orgId).doc(input.gateway)\n const day = outreachGatewayDay(input.atMs)\n try {\n await firestore.runTransaction(async (transaction) => {\n const [intelSnapshot, statsSnapshot] = await Promise.all([transaction.get(intelRef), transaction.get(statsRef)])\n const intel = readStoredOutreachDomainIntel(domain, intelSnapshot.exists ? intelSnapshot.data() : undefined)\n const stats = readStoredOutreachGatewayStats(input.gateway, statsSnapshot.exists ? statsSnapshot.data() : undefined)\n const blockedAt = input.outcome === 'blocked' ? input.atMs : null\n const nextIntel: OutreachDomainIntel = {\n domain,\n // A domain never looked up — enrolled before the intel existed —\n // is written with what the bounce said, and looked up on its next read.\n mx: intel?.mx ?? [],\n gateway: intel?.gateway ?? input.gateway,\n resolvedAtMs: intel?.resolvedAtMs ?? 0,\n sent: (intel?.sent ?? 0) + (input.outcome === 'sent' ? by : 0),\n delivered: (intel?.delivered ?? 0) + (input.outcome === 'delivered' ? by : 0),\n blocked: (intel?.blocked ?? 0) + (input.outcome === 'blocked' ? by : 0),\n lastBlockedAtMs: Math.max(intel?.lastBlockedAtMs ?? 0, blockedAt ?? 0) || null,\n updatedAtMs: input.atMs,\n }\n const days = pruneOutreachGatewayDays(stats?.days, input.atMs, OUTREACH_GATEWAY_WINDOW_DAYS)\n const today = days[day] ?? { sent: 0, delivered: 0, blocked: 0 }\n days[day] = { ...today, [input.outcome]: today[input.outcome] + by }\n const nextStats: OutreachGatewayStats = {\n gateway: input.gateway,\n sent: (stats?.sent ?? 0) + (input.outcome === 'sent' ? by : 0),\n delivered: (stats?.delivered ?? 0) + (input.outcome === 'delivered' ? by : 0),\n blocked: (stats?.blocked ?? 0) + (input.outcome === 'blocked' ? by : 0),\n lastBlockedAtMs: Math.max(stats?.lastBlockedAtMs ?? 0, blockedAt ?? 0) || null,\n days,\n updatedAtMs: input.atMs,\n }\n transaction.set(intelRef, nextIntel)\n transaction.set(statsRef, nextStats)\n })\n } catch (error) {\n console.error(`[outreach] the gateway ledger could not count a ${input.outcome} at ${domain}`, error)\n }\n}\n"],"names":["classifyOutreachMailGateway","OUTREACH_DOMAIN_INTEL_TTL_MS","OUTREACH_GATEWAY_CHIP_DAYS","OUTREACH_GATEWAY_WINDOW_DAYS","OUTREACH_MAIL_GATEWAYS","outreachGatewayDay","outreachGatewayWindow","pruneOutreachGatewayDays","normalizeOutreachDomain","outreachEmailDomain","outreachOrgCollection","GET_ALL_CHUNK","NO_MX_CODES","Set","count","value","number","Number","isFinite","Math","max","floor","ms","gatewayOf","includes","readStoredOutreachDomainIntel","id","data","gateway","domain","mx","Array","isArray","filter","entry","resolvedAtMs","sent","delivered","blocked","lastBlockedAtMs","updatedAtMs","readStoredOutreachGatewayStats","days","raw","day","counts","Object","entries","resolveOutreachDomainMx","resolveMx","records","error","code","String","toUpperCase","has","record","exchange","sort","a","b","priority","map","trim","toLowerCase","replace","intelCollection","firestore","orgId","statsCollection","readOutreachDomainIntel","emails","input","known","answers","Map","domainOf","email","set","domains","values","length","collection","start","chunk","slice","snapshots","getAll","doc","forEach","index","snapshot","exists","undefined","console","keys","Promise","all","stored","get","nowMs","resolved","fresh","merge","readOutreachGatewayStats","gateways","wanted","outreachGatewayStandingOf","intel","stats","week","month","blocked7","delivered7","blocked30","delivered30","readOutreachGatewayStandings","recordOutreachGatewayOutcome","by","intelRef","statsRef","atMs","runTransaction","transaction","intelSnapshot","statsSnapshot","blockedAt","outcome","nextIntel","today","nextStats"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/storage/domain-intel-store.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 SEQUENCES KNOWS ABOUT THE DOMAINS IT WRITES TO (AGL-3326).\n *\n * Both halves live on the platform (AGL-3328), in `@aglyn/tenant-data-admin`'s\n * `email-deliverability`, which the Resend path reads and writes too:\n *\n * - a recipient domain's MX — the platform-wide `mailDomains/{domain}`\n * cache, looked up with the resolver the caller hands in and trusted for\n * a week (a day for a domain that takes no mail). The first workspace to\n * write to a domain warms it for every other;\n * - the ledger — `orgs/{orgId}/mailGatewayLedger/{sendingDomain}~{gateway}`,\n * keyed by the MAILBOX's sending domain, because a gateway refuses a\n * sender: the Barracuda that refused `aglyn.io` refuses `aglyn.io`, not\n * the organization's other mailbox on another domain. It stays under the\n * organization because a mailbox's domain is not one organization's — two\n * workspaces can each connect a `gmail.com` mailbox.\n *\n * A read that could not be made answers `null` for the addresses it\n * covered, which the gates refuse as \"we couldn't check\" — except that a\n * stale answer that the domain takes mail outranks no answer. A domain that\n * resolves to no MX is `none`, and the gates block it.\n *\n * ## The two collections this module wrote first\n *\n * `outreachDomainIntel` and `outreachGatewayStats` are READ THROUGH, not\n * migrated. The first held MX answers, which a lookup re-derives, so it is\n * no longer read. The second held a per-gateway ledger with no sending\n * domain; its days are summed into every sending domain's standing for the\n * thirty days they can still hold a send — the conservative reading, since\n * an organization with one mailbox domain loses nothing and one with two\n * holds a little more. Nothing writes either any more; both are retired,\n * with their rules, once the window has passed.\n */\n\nimport {\n type MailDnsResolver,\n type MailDomainIntel,\n type MailGatewayOutcome,\n mailGatewayStanding,\n normalizeLedgerSendingDomain,\n resolveMailDomain,\n} from '@aglyn/shared-util-email'\nimport {\n readMailDomains,\n readMailGatewayLedgers,\n recordMailGatewayOutcome,\n} from '@aglyn/tenant-data-admin/server/email-deliverability'\nimport type { OutreachGatewayStanding, OutreachMailGateway } from '../engine/mail-gateway'\nimport { OUTREACH_MAIL_GATEWAYS } from '../engine/mail-gateway'\nimport { outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport type { OutreachGatewayStats, OutreachMailbox } from '../model/outreach.types'\nimport { outreachOrgCollection } from './outreach-records'\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** One MX record as Node's resolver answers it. */\nexport interface OutreachMxRecord {\n exchange: string\n priority: number\n}\n\n/**\n * `dns.promises.resolveMx`, or a stand-in: the platform hands in its own\n * resolver, the specs hand in a table. Rejects the way Node does — `code`\n * `ENOTFOUND` or `ENODATA` for a domain with no MX — and any other\n * rejection is a resolver that could not answer.\n */\nexport type OutreachResolveMx = (domain: string) => Promise<ReadonlyArray<OutreachMxRecord>>\n\n/** What a domain-intel read needs beside the store. */\nexport interface OutreachDomainIntelReadInput {\n resolveMx: OutreachResolveMx\n /**\n * Whether a domain with no MX has an address record — the implicit MX\n * RFC 5321 still delivers to. Omitted, a domain with no MX reads as none.\n */\n resolveAddress?: (domain: string) => Promise<boolean>\n nowMs: number\n /**\n * The domain the mailbox sends from (AGL-3328): the ledger is keyed by\n * it. Omitted or `null`, no ledger is read and nothing is held.\n */\n sendingDomain?: string | null\n}\n\n/** The resolver the platform store reads with, from what a caller hands in. */\nfunction resolverOf(input: Pick<OutreachDomainIntelReadInput, 'resolveMx' | 'resolveAddress'>): MailDnsResolver {\n return {\n resolveMx: input.resolveMx,\n ...(input.resolveAddress ? { resolveAddress: input.resolveAddress } : {}),\n }\n}\n\n/** The domain a mailbox's mail leaves from: its send-as address's, else the account's. */\nexport function outreachMailboxSendingDomain(\n mailbox: Pick<OutreachMailbox, 'sendAs' | 'email'> | null | undefined,\n): string | null {\n return normalizeLedgerSendingDomain(mailbox?.sendAs || mailbox?.email)\n}\n\n/**\n * A domain's MX, resolved and classified. A domain the resolver says has no\n * MX answers `none`; a resolver that could not answer rejects, and the\n * caller decides what a missing answer means.\n */\nexport async function resolveOutreachDomainMx(\n resolveMx: OutreachResolveMx,\n domain: string,\n): Promise<{ mx: string[]; gateway: OutreachMailGateway }> {\n const intel = await resolveMailDomain({ resolveMx }, domain, 0)\n return { mx: intel.mx, gateway: intel.gateway }\n}\n\n/**\n * The MX answer for every address, keyed by address — read through the\n * platform cache, and looked up and stored when missing or stale. An\n * address with no domain, and one whose domain could not be looked up and\n * was never known, answers `null`.\n */\nexport async function readOutreachDomainIntel(\n firestore: Firestore,\n _orgId: string,\n emails: readonly string[],\n input: OutreachDomainIntelReadInput,\n): Promise<Map<string, MailDomainIntel | null>> {\n const answers = new Map<string, MailDomainIntel | null>()\n const domainOf = new Map<string, string>()\n for (const email of emails) {\n const domain = outreachEmailDomain(email)\n if (domain) domainOf.set(email, domain)\n else answers.set(email, null)\n }\n if (!domainOf.size) return answers\n let intel: Map<string, MailDomainIntel | null>\n try {\n intel = await readMailDomains([...new Set(domainOf.values())], {\n firestore,\n resolver: resolverOf(input),\n nowMs: input.nowMs,\n })\n } catch (error) {\n console.error('[outreach] domain intel lookup failed; reading as unchecked', error)\n intel = new Map()\n }\n for (const [email, domain] of domainOf) answers.set(email, intel.get(domain) ?? null)\n return answers\n}\n\nconst count = (value: unknown): number => {\n const number = Number(value)\n return Number.isFinite(number) ? Math.max(0, Math.floor(number)) : 0\n}\nconst ms = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null\nconst gatewayOf = (value: unknown): OutreachMailGateway | null =>\n (OUTREACH_MAIL_GATEWAYS as readonly unknown[]).includes(value) ? (value as OutreachMailGateway) : null\n\n/** A stored per-gateway ledger from before the sending domain keyed it, or `null` for no document. */\nexport function readStoredOutreachGatewayStats(\n id: string,\n data: Record<string, unknown> | undefined,\n): OutreachGatewayStats | null {\n if (!data) return null\n const gateway = gatewayOf(data['gateway']) ?? gatewayOf(id)\n if (!gateway) return null\n const days: OutreachGatewayStats['days'] = {}\n const raw = data['days']\n if (raw && typeof raw === 'object') {\n for (const [day, counts] of Object.entries(raw as Record<string, unknown>)) {\n const entry = (counts && typeof counts === 'object' ? counts : {}) as Record<string, unknown>\n days[day] = { sent: count(entry['sent']), delivered: count(entry['delivered']), blocked: count(entry['blocked']) }\n }\n }\n return {\n gateway,\n sent: count(data['sent']),\n delivered: count(data['delivered']),\n blocked: count(data['blocked']),\n lastBlockedAtMs: ms(data['lastBlockedAtMs']),\n days,\n updatedAtMs: ms(data['updatedAtMs']) ?? 0,\n }\n}\n\n/** The per-gateway ledgers written before the sending domain keyed them — see the module note. */\nasync function readLegacyGatewayStats(\n firestore: Firestore,\n orgId: string,\n gateways: readonly OutreachMailGateway[],\n): Promise<Map<OutreachMailGateway, OutreachGatewayStats | null>> {\n const answers = new Map<OutreachMailGateway, OutreachGatewayStats | null>()\n const wanted = [...new Set(gateways)].filter((gateway) => gateway !== 'none')\n if (!wanted.length) return answers\n const collection = outreachOrgCollection(firestore, orgId, 'gatewayStats')\n const snapshots = await firestore.getAll(...wanted.map((gateway) => collection.doc(gateway)))\n wanted.forEach((gateway, index) => {\n const snapshot = snapshots[index]\n answers.set(gateway, readStoredOutreachGatewayStats(gateway, snapshot?.exists ? snapshot.data() : undefined))\n })\n return answers\n}\n\n/** Two standings for one gateway, summed. */\nfunction sumStandings(a: OutreachGatewayStanding, b: OutreachGatewayStanding): OutreachGatewayStanding {\n return {\n gateway: a.gateway,\n blocked7: a.blocked7 + b.blocked7,\n delivered7: a.delivered7 + b.delivered7,\n blocked30: a.blocked30 + b.blocked30,\n delivered30: a.delivered30 + b.delivered30,\n }\n}\n\n/**\n * The gateway standing of every address, keyed by address: the MX answer\n * read (and refreshed) above, joined to the mailbox's sending-domain ledger\n * with each gateway it names, plus what the legacy per-gateway ledger still\n * holds. `null` for an address whose domain could not be checked.\n */\nexport async function readOutreachGatewayStandings(\n firestore: Firestore,\n orgId: string,\n emails: readonly string[],\n input: OutreachDomainIntelReadInput,\n): Promise<Map<string, OutreachGatewayStanding | null>> {\n const intel = await readOutreachDomainIntel(firestore, orgId, emails, input)\n const gateways = [...intel.values()]\n .filter((entry): entry is MailDomainIntel => entry !== null)\n .map((entry) => entry.gateway)\n const sendingDomain = normalizeLedgerSendingDomain(input.sendingDomain)\n let ledgers: Awaited<ReturnType<typeof readMailGatewayLedgers>>\n let legacy: Map<OutreachMailGateway, OutreachGatewayStats | null>\n try {\n ;[ledgers, legacy] = await Promise.all([\n sendingDomain\n ? readMailGatewayLedgers({ orgId }, sendingDomain, gateways, { firestore, nowMs: input.nowMs })\n : Promise.resolve(new Map()),\n readLegacyGatewayStats(firestore, orgId, gateways),\n ])\n } catch (error) {\n console.error('[outreach] gateway ledger lookup failed; reading as unchecked', error)\n return new Map(emails.map((email) => [email, null]))\n }\n const answers = new Map<string, OutreachGatewayStanding | null>()\n for (const email of emails) {\n const entry = intel.get(email) ?? null\n if (!entry) {\n answers.set(email, null)\n continue\n }\n const current = mailGatewayStanding(entry.gateway, ledgers.get(entry.gateway) ?? null, input.nowMs)\n const previous = mailGatewayStanding(entry.gateway, legacy.get(entry.gateway) ?? null, input.nowMs)\n answers.set(email, sumStandings(current, previous))\n }\n return answers\n}\n\n/** What a send met at a gateway, as the ledger counts it. */\nexport type OutreachGatewayOutcome = MailGatewayOutcome\n\n/**\n * Counts an outcome on the mailbox's sending-domain ledger with a gateway.\n * Never throws: the send or the bounce it records has already happened,\n * and a lost count understates a gateway while a thrown one loses the step\n * that called. A mailbox whose sending domain cannot be read counts nothing.\n */\nexport async function recordOutreachGatewayOutcome(\n firestore: Firestore,\n orgId: string,\n input: {\n sendingDomain: string | null\n gateway: OutreachMailGateway\n outcome: OutreachGatewayOutcome\n count?: number\n atMs: number\n /** A refusal's diagnostic, kept scrubbed of addresses. */\n detail?: string | null\n },\n): Promise<void> {\n const sendingDomain = normalizeLedgerSendingDomain(input.sendingDomain)\n if (!sendingDomain) return\n await recordMailGatewayOutcome(\n { orgId },\n {\n sendingDomain,\n gateway: input.gateway,\n outcome: input.outcome,\n count: input.count,\n atMs: input.atMs,\n detail: input.detail ?? null,\n },\n { firestore },\n )\n}\n"],"names":["mailGatewayStanding","normalizeLedgerSendingDomain","resolveMailDomain","readMailDomains","readMailGatewayLedgers","recordMailGatewayOutcome","OUTREACH_MAIL_GATEWAYS","outreachEmailDomain","outreachOrgCollection","resolverOf","input","resolveMx","resolveAddress","outreachMailboxSendingDomain","mailbox","sendAs","email","resolveOutreachDomainMx","domain","intel","mx","gateway","readOutreachDomainIntel","firestore","_orgId","emails","answers","Map","domainOf","set","size","Set","values","resolver","nowMs","error","console","get","count","value","number","Number","isFinite","Math","max","floor","ms","gatewayOf","includes","readStoredOutreachGatewayStats","id","data","days","raw","day","counts","Object","entries","entry","sent","delivered","blocked","lastBlockedAtMs","updatedAtMs","readLegacyGatewayStats","orgId","gateways","wanted","filter","length","collection","snapshots","getAll","map","doc","forEach","index","snapshot","exists","undefined","sumStandings","a","b","blocked7","delivered7","blocked30","delivered30","readOutreachGatewayStandings","sendingDomain","ledgers","legacy","Promise","all","resolve","current","previous","recordOutreachGatewayOutcome","outcome","atMs","detail"],"mappings":"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"}
|