@aglyn/shared-util-email 1.0.0-beta.224 → 1.0.0-beta.225

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/shared-util-email",
3
- "version": "1.0.0-beta.224",
3
+ "version": "1.0.0-beta.225",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,8 +25,8 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/shared-data-enums": "1.0.0-beta.224",
29
- "@aglyn/shared-util-http": "1.0.0-beta.224",
28
+ "@aglyn/shared-data-enums": "1.0.0-beta.225",
29
+ "@aglyn/shared-util-http": "1.0.0-beta.225",
30
30
  "@msgpack/msgpack": "^3.1.3",
31
31
  "@swc/helpers": "0.5.23"
32
32
  },
@@ -100,6 +100,15 @@ export declare function resendSenderExpectation(record: {
100
100
  * `null` for a consumer Gmail address or an unreadable one.
101
101
  */
102
102
  export declare function googleMailboxSenderExpectation(address: string | null | undefined): SenderReadinessExpectation | null;
103
+ /**
104
+ * A Microsoft 365 mailbox's expectation (AGL-3489): mail From the address's
105
+ * domain, sent through Exchange Online with the envelope on the same domain,
106
+ * SPF naming `spf.protection.outlook.com`, and the key under `selector1` —
107
+ * the CNAME Microsoft 365 publishes DKIM through, which a TXT lookup
108
+ * follows. `null` for a consumer Outlook address, an `onmicrosoft.com`
109
+ * tenant address, or an unreadable one: their records are Microsoft's own.
110
+ */
111
+ export declare function microsoftMailboxSenderExpectation(address: string | null | undefined): SenderReadinessExpectation | null;
103
112
  /** The TXT answers a readiness read needs; `null` for a lookup nobody answered. */
104
113
  export interface SenderReadinessObservation {
105
114
  spfTxt: readonly string[] | null;
@@ -154,6 +154,34 @@ const DMARC_POLICIES = [
154
154
  dkimDomain: domain
155
155
  };
156
156
  }
157
+ /** Microsoft's consumer domains, and the tenant domains it names itself. */ const MICROSOFT_CONSUMER_DOMAINS = new Set([
158
+ 'outlook.com',
159
+ 'hotmail.com',
160
+ 'live.com',
161
+ 'msn.com'
162
+ ]);
163
+ /**
164
+ * A Microsoft 365 mailbox's expectation (AGL-3489): mail From the address's
165
+ * domain, sent through Exchange Online with the envelope on the same domain,
166
+ * SPF naming `spf.protection.outlook.com`, and the key under `selector1` —
167
+ * the CNAME Microsoft 365 publishes DKIM through, which a TXT lookup
168
+ * follows. `null` for a consumer Outlook address, an `onmicrosoft.com`
169
+ * tenant address, or an unreadable one: their records are Microsoft's own.
170
+ */ export function microsoftMailboxSenderExpectation(address) {
171
+ const raw = String(address != null ? address : '').trim().toLowerCase();
172
+ const domain = normalizeSendingDomain(raw.slice(raw.lastIndexOf('@') + 1));
173
+ if (!raw.includes('@') || !domain || MICROSOFT_CONSUMER_DOMAINS.has(domain) || domain.endsWith('.onmicrosoft.com')) {
174
+ return null;
175
+ }
176
+ return {
177
+ provider: 'Microsoft 365',
178
+ fromDomain: domain,
179
+ envelopeDomain: domain,
180
+ spfInclude: 'spf.protection.outlook.com',
181
+ dkimSelector: 'selector1',
182
+ dkimDomain: domain
183
+ };
184
+ }
157
185
  const STATE_RANK = {
158
186
  pass: 0,
159
187
  unknown: 1,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/sender-readiness.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { registrableDomain } from './outbound-phishing-screen'\nimport {\n normalizeSendingDomain,\n SENDING_SUBDOMAIN,\n sendingSpfInclude,\n} from './sending-domain'\n\n/*==========================================\n * SENDER READINESS — SPF, DKIM, DMARC and whether they line up.\n *\n * The recipient's side of a send is the deliverability engine's\n * (`email-deliverability.ts`); this is the sender's. A receiver decides\n * whether mail from a domain is really that domain's by three records the\n * SENDER publishes:\n *\n * - SPF, on the envelope (Return-Path) domain, naming the servers that may\n * send for it;\n * - DKIM, a public key at `<selector>._domainkey.<domain>`, which the\n * signature's `d=` names;\n * - DMARC, on the From domain (or its organizational domain), which says\n * what to do with mail that fails, and which only passes when SPF or\n * DKIM passes FOR A DOMAIN THAT ALIGNS WITH THE FROM DOMAIN.\n *\n * Alignment is the part a records table cannot show: every record can be\n * present and the mail can still fail DMARC, because the domain that passed\n * is not the one in the From line. So the verdict here is per record and\n * then aligned — relaxed alignment is \"same organizational domain\", strict\n * is \"the same name\", as `adkim=` / `aspf=` ask.\n *\n * Pure: the lookups are the server layer's (`sender-readiness.ts` in\n * `@aglyn/tenant-data-admin`), handed in as TXT answers, `null` for a lookup\n * nobody answered. An unanswered lookup is `unknown`, never `fail` — a\n * resolver outage must not tell a customer to rebuild a working zone.\n *\n * The sending-domain verification (`assessSendingRecords`) already decides\n * whether a Resend domain may SEND, on exact records. This reads the same\n * zone for a different question — will receivers believe it — and never\n * changes a domain's status.\n *==========================================*/\n\n/** How a check came out. `unknown` is a lookup nobody answered. */\nexport type SenderReadinessState = 'pass' | 'warn' | 'fail' | 'unknown'\n\n/** The `all` mechanism an SPF record ends with. */\nexport type SpfAllQualifier = '-all' | '~all' | '?all' | '+all'\n\n/** An SPF record, read. */\nexport interface SpfPolicy {\n /** The `v=spf1` record, or null when there is none (or more than one). */\n record: string | null\n /** How many `v=spf1` records the name publishes; more than one is a permanent error. */\n count: number\n /** Every `include:` target, lowercased. */\n includes: string[]\n /** The `redirect=` target, or null. */\n redirect: string | null\n /** The closing `all`, or null when the record has none. */\n all: SpfAllQualifier | null\n}\n\n/** Read a TXT answer as an SPF policy. Only `v=spf1` records count. */\nexport function parseSpf(records: readonly string[] | null | undefined): SpfPolicy {\n const found = (records ?? [])\n .map((entry) => String(entry ?? '').trim())\n .filter((entry) => /^v\\s*=\\s*spf1(\\s|$)/i.test(entry))\n if (found.length !== 1) {\n return { record: null, count: found.length, includes: [], redirect: null, all: null }\n }\n const record = found[0]\n const terms = record.split(/\\s+/).slice(1).map((term) => term.toLowerCase())\n const includes = terms\n .filter((term) => /^[+?~-]?include:/.test(term))\n .map((term) => term.slice(term.indexOf(':') + 1).replace(/\\.$/, ''))\n const redirect = terms.find((term) => term.startsWith('redirect='))?.slice('redirect='.length).replace(/\\.$/, '') ?? null\n const allTerm = terms.find((term) => /^[+?~-]?all$/.test(term)) ?? null\n const all = allTerm ? ((allTerm === 'all' ? '+all' : allTerm) as SpfAllQualifier) : null\n return { record, count: 1, includes, redirect, all }\n}\n\n/** Whether an SPF policy names `include` directly, as an include or its redirect. */\nexport function spfNamesInclude(policy: SpfPolicy, include: string): boolean {\n const wanted = String(include ?? '').trim().toLowerCase().replace(/\\.$/, '')\n if (!wanted) return false\n return policy.includes.includes(wanted) || policy.redirect === wanted\n}\n\n/** DMARC alignment mode: relaxed (same organizational domain) or strict (same name). */\nexport type DmarcAlignmentMode = 'r' | 's'\n\n/** A DMARC record, read. */\nexport interface DmarcRecordPolicy {\n record: string | null\n /** `p=`; `absent` when no record applies. */\n policy: 'reject' | 'quarantine' | 'none' | 'absent'\n /** `sp=`, the policy for subdomains, or null when the record sets none. */\n subdomainPolicy: 'reject' | 'quarantine' | 'none' | null\n adkim: DmarcAlignmentMode\n aspf: DmarcAlignmentMode\n /** `rua=` addresses, the aggregate reports' destinations. */\n rua: string[]\n /** `pct=`, 100 when unset. */\n pct: number\n}\n\nconst DMARC_POLICIES = ['reject', 'quarantine', 'none'] as const\ntype DmarcPolicyValue = (typeof DMARC_POLICIES)[number]\n\n/**\n * Read a `_dmarc` TXT answer. Only a record that begins `v=DMARC1` counts, as\n * in `assessDmarc`; tags are read case-insensitively and a tag it does not\n * know is ignored.\n */\nexport function parseDmarcRecord(records: readonly string[] | null | undefined): DmarcRecordPolicy {\n const found = (records ?? [])\n .map((entry) => String(entry ?? '').trim())\n .find((entry) => /^v\\s*=\\s*DMARC1\\b/i.test(entry))\n if (!found) {\n return { record: null, policy: 'absent', subdomainPolicy: null, adkim: 'r', aspf: 'r', rua: [], pct: 100 }\n }\n const tags = new Map<string, string>()\n for (const part of found.split(';')) {\n const at = part.indexOf('=')\n if (at < 0) continue\n tags.set(part.slice(0, at).trim().toLowerCase(), part.slice(at + 1).trim())\n }\n const policyOf = (value: string | undefined): DmarcPolicyValue | null => {\n const lowered = String(value ?? '').toLowerCase()\n return (DMARC_POLICIES as readonly string[]).includes(lowered) ? (lowered as DmarcPolicyValue) : null\n }\n const mode = (value: string | undefined): DmarcAlignmentMode => (String(value ?? '').toLowerCase() === 's' ? 's' : 'r')\n const pct = Number(tags.get('pct'))\n return {\n record: found,\n // A record with an unreadable `p=` is treated as `none`, which is what a\n // receiver does with it.\n policy: policyOf(tags.get('p')) ?? 'none',\n subdomainPolicy: policyOf(tags.get('sp')),\n adkim: mode(tags.get('adkim')),\n aspf: mode(tags.get('aspf')),\n rua: String(tags.get('rua') ?? '')\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean),\n pct: Number.isFinite(pct) ? Math.min(100, Math.max(0, pct)) : 100,\n }\n}\n\n/** Whether two domains align under a DMARC mode. */\nexport function dmarcDomainsAlign(a: string, b: string, mode: DmarcAlignmentMode): boolean {\n const left = normalizeSendingDomain(a)\n const right = normalizeSendingDomain(b)\n if (!left || !right) return false\n if (mode === 's') return left === right\n return registrableDomain(left) === registrableDomain(right)\n}\n\n/**\n * What a sender publishes for one provider to send as one From domain: where\n * each record is expected, and what it must name.\n */\nexport interface SenderReadinessExpectation {\n /** Who sends the mail, for the sentences. */\n provider: string\n /** The From domain. */\n fromDomain: string\n /** The envelope (Return-Path) domain, whose SPF a receiver checks. */\n envelopeDomain: string\n /** The `include:` the envelope domain's SPF must carry for this provider. */\n spfInclude: string\n /** The selector the provider signs with. */\n dkimSelector: string\n /** The signature's `d=`, where the key is published. */\n dkimDomain: string\n}\n\n/** A readiness expectation's DNS names, for the server layer to look up. */\nexport function senderReadinessHosts(expectation: SenderReadinessExpectation): {\n spf: string\n dkim: string\n dmarc: string\n organizationalDmarc: string | null\n} {\n const from = normalizeSendingDomain(expectation.fromDomain)\n const organizational = registrableDomain(from)\n return {\n spf: normalizeSendingDomain(expectation.envelopeDomain),\n dkim: `${expectation.dkimSelector}._domainkey.${normalizeSendingDomain(expectation.dkimDomain)}`,\n dmarc: `_dmarc.${from}`,\n organizationalDmarc: organizational && organizational !== from ? `_dmarc.${organizational}` : null,\n }\n}\n\n/**\n * A Resend sending domain's expectation: mail From `<domain>`, its envelope\n * on `send.<domain>` (the subdomain the records table asks for), signed\n * `d=<domain>` with the domain's issued selector.\n */\nexport function resendSenderExpectation(record: {\n domain: string\n dkimSelector?: string | null\n}): SenderReadinessExpectation | null {\n const domain = normalizeSendingDomain(record?.domain ?? '')\n if (!domain) return null\n return {\n provider: 'the mail provider',\n fromDomain: domain,\n envelopeDomain: `${SENDING_SUBDOMAIN}.${domain}`,\n spfInclude: sendingSpfInclude(),\n dkimSelector: String(record?.dkimSelector ?? '').trim() || 'aglyn',\n dkimDomain: domain,\n }\n}\n\n/**\n * Domains Google itself authenticates. A consumer Gmail address has nothing\n * for its owner to publish, so it has no readiness to show.\n */\nconst GOOGLE_CONSUMER_DOMAINS: ReadonlySet<string> = new Set(['gmail.com', 'googlemail.com'])\n\n/**\n * A Google Workspace mailbox's expectation: mail From the address's domain,\n * sent through Google with the envelope on the same domain, SPF naming\n * `_spf.google.com` and the key under Google's default `google` selector.\n * `null` for a consumer Gmail address or an unreadable one.\n */\nexport function googleMailboxSenderExpectation(address: string | null | undefined): SenderReadinessExpectation | null {\n const raw = String(address ?? '').trim().toLowerCase()\n const domain = normalizeSendingDomain(raw.slice(raw.lastIndexOf('@') + 1))\n if (!raw.includes('@') || !domain || GOOGLE_CONSUMER_DOMAINS.has(domain)) return null\n return {\n provider: 'Google Workspace',\n fromDomain: domain,\n envelopeDomain: domain,\n spfInclude: '_spf.google.com',\n dkimSelector: 'google',\n dkimDomain: domain,\n }\n}\n\n/** The TXT answers a readiness read needs; `null` for a lookup nobody answered. */\nexport interface SenderReadinessObservation {\n spfTxt: readonly string[] | null\n dkimTxt: readonly string[] | null\n dmarcTxt: readonly string[] | null\n /**\n * `_dmarc` at the organizational domain, asked only when the From domain\n * is a subdomain and publishes no record of its own. `undefined` when it\n * was not asked.\n */\n organizationalDmarcTxt?: readonly string[] | null\n}\n\n/** One check, with the sentence a card shows for it. */\nexport interface SenderReadinessCheck {\n state: SenderReadinessState\n /** Where the record was looked for. */\n host: string\n /** The record as published, when there is one. */\n record: string | null\n detail: string\n}\n\nexport interface SenderReadiness {\n provider: string\n fromDomain: string\n spf: SenderReadinessCheck & { all: SpfAllQualifier | null; authorizes: boolean }\n dkim: SenderReadinessCheck & { selector: string }\n dmarc: SenderReadinessCheck & {\n policy: DmarcRecordPolicy['policy']\n rua: string[]\n /** The domain whose record applies — the From domain's, or its organizational domain's. */\n policyDomain: string\n }\n /** Whether DMARC passes, and on which mechanism. */\n alignment: SenderReadinessCheck & { spfAligned: boolean | null; dkimAligned: boolean | null }\n /** The worst of the four. */\n overall: SenderReadinessState\n checkedAtMs: number\n}\n\nconst STATE_RANK: Record<SenderReadinessState, number> = { pass: 0, unknown: 1, warn: 2, fail: 3 }\n\n/** The worst of several states: fail over warn over unknown over pass. */\nexport function worstSenderReadinessState(states: readonly SenderReadinessState[]): SenderReadinessState {\n return states.reduce<SenderReadinessState>((worst, state) => (STATE_RANK[state] > STATE_RANK[worst] ? state : worst), 'pass')\n}\n\nfunction assessSpf(expectation: SenderReadinessExpectation, host: string, txt: readonly string[] | null): SenderReadiness['spf'] {\n if (!txt) {\n return { state: 'unknown', host, record: null, all: null, authorizes: false, detail: `The SPF lookup for ${host} went unanswered.` }\n }\n const policy = parseSpf(txt)\n if (policy.count > 1) {\n return {\n state: 'fail',\n host,\n record: null,\n all: null,\n authorizes: false,\n detail: `${host} publishes ${policy.count} SPF records. Receivers treat more than one as an error and SPF fails for every message; merge them into one.`,\n }\n }\n if (!policy.record) {\n return {\n state: 'fail',\n host,\n record: null,\n all: null,\n authorizes: false,\n detail: `${host} publishes no SPF record, so no receiver can tell ${expectation.provider} is allowed to send for it.`,\n }\n }\n const authorizes = spfNamesInclude(policy, expectation.spfInclude)\n const base = { host, record: policy.record, all: policy.all, authorizes }\n if (policy.all === '+all') {\n return {\n ...base,\n state: 'fail',\n detail: 'The record ends in +all, which authorizes every server on the internet to send as this domain.',\n }\n }\n if (!authorizes) {\n return {\n ...base,\n state: 'warn',\n detail:\n `The record does not name include:${expectation.spfInclude}. If it reaches it through another ` +\n `include this is fine; otherwise mail sent by ${expectation.provider} fails SPF.`,\n }\n }\n if (policy.all === '-all') {\n return { ...base, state: 'pass', detail: `Authorizes ${expectation.provider} and refuses every other server (-all).` }\n }\n if (policy.all === '~all') {\n return {\n ...base,\n state: 'pass',\n detail: `Authorizes ${expectation.provider}; other servers soft-fail (~all), the usual setting while DMARC does the enforcing.`,\n }\n }\n if (!policy.all && policy.redirect) {\n return { ...base, state: 'pass', detail: `Authorizes ${expectation.provider} through redirect=${policy.redirect}.` }\n }\n return {\n ...base,\n state: 'warn',\n detail:\n policy.all === '?all'\n ? `Authorizes ${expectation.provider}, but ?all says nothing about every other server. End it in ~all or -all.`\n : `Authorizes ${expectation.provider}, but the record has no closing all, which reads as ?all. End it in ~all or -all.`,\n }\n}\n\nfunction assessDkim(expectation: SenderReadinessExpectation, host: string, txt: readonly string[] | null): SenderReadiness['dkim'] {\n const selector = expectation.dkimSelector\n if (!txt) return { state: 'unknown', host, record: null, selector, detail: `The DKIM lookup for ${host} went unanswered.` }\n const keys = txt.map((entry) => String(entry ?? '').trim()).filter((entry) => /(^|;)\\s*p\\s*=/i.test(entry))\n const live = keys.find((entry) => /(^|;)\\s*p\\s*=\\s*[A-Za-z0-9+/]/.test(entry))\n if (live) {\n return { state: 'pass', host, record: live, selector, detail: `A signing key is published for the ${selector} selector.` }\n }\n if (keys.length) {\n return {\n state: 'fail',\n host,\n record: keys[0],\n selector,\n detail: `The ${selector} selector's key is empty (p=), which revokes it. Mail signed with it fails DKIM.`,\n }\n }\n return {\n state: 'fail',\n host,\n record: null,\n selector,\n detail: `No DKIM key is published for the ${selector} selector, so ${expectation.provider}'s signature cannot be checked.`,\n }\n}\n\nfunction assessDmarcRecord(\n expectation: SenderReadinessExpectation,\n hosts: ReturnType<typeof senderReadinessHosts>,\n observation: SenderReadinessObservation,\n): { check: SenderReadiness['dmarc']; policy: DmarcRecordPolicy | null } {\n const from = normalizeSendingDomain(expectation.fromDomain)\n if (!observation.dmarcTxt) {\n return {\n check: {\n state: 'unknown',\n host: hosts.dmarc,\n record: null,\n policy: 'absent',\n rua: [],\n policyDomain: from,\n detail: `The DMARC lookup for ${hosts.dmarc} went unanswered.`,\n },\n policy: null,\n }\n }\n let parsed = parseDmarcRecord(observation.dmarcTxt)\n let host = hosts.dmarc\n let policyDomain = from\n let inherited = false\n if (!parsed.record && hosts.organizationalDmarc) {\n if (observation.organizationalDmarcTxt === null) {\n return {\n check: {\n state: 'unknown',\n host: hosts.organizationalDmarc,\n record: null,\n policy: 'absent',\n rua: [],\n policyDomain: registrableDomain(from),\n detail: `The DMARC lookup for ${hosts.organizationalDmarc} went unanswered.`,\n },\n policy: null,\n }\n }\n const organizational = parseDmarcRecord(observation.organizationalDmarcTxt ?? [])\n if (organizational.record) {\n // A subdomain with no record of its own takes the organizational\n // domain's, under its `sp=` when it sets one.\n parsed = { ...organizational, policy: organizational.subdomainPolicy ?? organizational.policy }\n host = hosts.organizationalDmarc\n policyDomain = registrableDomain(from)\n inherited = true\n }\n }\n const base = { host, record: parsed.record, policy: parsed.policy, rua: parsed.rua, policyDomain }\n if (!parsed.record) {\n return {\n check: {\n ...base,\n state: 'warn',\n detail:\n `${from} publishes no DMARC policy. Gmail and Yahoo expect one from anyone sending in bulk, ` +\n 'and without it anyone can send mail claiming to be from this domain.',\n },\n policy: parsed,\n }\n }\n const inheritedNote = inherited ? ` (inherited from ${policyDomain})` : ''\n const reports = parsed.rua.length\n ? ` Aggregate reports go to ${parsed.rua.join(', ')}.`\n : ' It names no rua address, so no aggregate reports reach anyone.'\n if (parsed.policy === 'none') {\n return {\n check: {\n ...base,\n state: parsed.rua.length ? 'pass' : 'warn',\n detail: `p=none${inheritedNote}: DMARC monitors and enforces nothing.${reports}`,\n },\n policy: parsed,\n }\n }\n return {\n check: {\n ...base,\n state: 'pass',\n detail: `p=${parsed.policy}${parsed.pct < 100 ? ` at pct=${parsed.pct}` : ''}${inheritedNote}: mail that fails DMARC is ${\n parsed.policy === 'reject' ? 'refused' : 'sent to spam'\n }.${reports}`,\n },\n policy: parsed,\n }\n}\n\n/**\n * Whether one provider's mail as one From domain will be believed: each\n * record, and then whether DMARC passes on a mechanism that aligns.\n */\nexport function assessSenderReadiness(\n expectation: SenderReadinessExpectation,\n observation: SenderReadinessObservation,\n nowMs: number,\n): SenderReadiness {\n const hosts = senderReadinessHosts(expectation)\n const spf = assessSpf(expectation, hosts.spf, observation.spfTxt)\n const dkim = assessDkim(expectation, hosts.dkim, observation.dkimTxt)\n const { check: dmarc, policy } = assessDmarcRecord(expectation, hosts, observation)\n\n const adkim = policy?.adkim ?? 'r'\n const aspf = policy?.aspf ?? 'r'\n const dkimAligned =\n dkim.state === 'unknown' ? null : dkim.state === 'pass' && dmarcDomainsAlign(expectation.dkimDomain, expectation.fromDomain, adkim)\n const spfAligned =\n spf.state === 'unknown'\n ? null\n : spf.authorizes && spf.state !== 'fail' && dmarcDomainsAlign(expectation.envelopeDomain, expectation.fromDomain, aspf)\n const enforced = policy?.policy === 'reject' || policy?.policy === 'quarantine'\n let alignment: SenderReadiness['alignment']\n const host = normalizeSendingDomain(expectation.fromDomain)\n if (dkimAligned) {\n alignment = {\n state: 'pass',\n host,\n record: null,\n spfAligned,\n dkimAligned,\n detail: `DMARC passes on DKIM: the signature's domain lines up with the From domain${\n adkim === 's' ? ' exactly (adkim=s)' : ''\n }, and a signature survives forwarding.`,\n }\n } else if (spfAligned) {\n alignment = {\n state: 'warn',\n host,\n record: null,\n spfAligned,\n dkimAligned,\n detail:\n 'DMARC passes on SPF alone. Forwarding breaks SPF, so forwarded mail fails DMARC until the DKIM key is published.',\n }\n } else if (dkimAligned === null || spfAligned === null) {\n alignment = {\n state: 'unknown',\n host,\n record: null,\n spfAligned,\n dkimAligned,\n detail: 'Alignment cannot be judged until the lookups above are answered.',\n }\n } else {\n const strictMiss =\n (adkim === 's' && dkim.state === 'pass') || (aspf === 's' && spf.authorizes)\n ? ' The DMARC record asks for strict alignment, which only an exact name satisfies.'\n : ''\n alignment = {\n state: 'fail',\n host,\n record: null,\n spfAligned,\n dkimAligned,\n detail: `Neither SPF nor DKIM passes for a domain that lines up with ${host}, so DMARC fails${\n enforced ? ` and p=${policy?.policy} ${policy?.policy === 'reject' ? 'refuses' : 'quarantines'} the mail` : ''\n }.${strictMiss}`,\n }\n }\n\n return {\n provider: expectation.provider,\n fromDomain: host,\n spf,\n dkim,\n dmarc,\n alignment,\n overall: worstSenderReadinessState([spf.state, dkim.state, dmarc.state, alignment.state]),\n checkedAtMs: nowMs,\n }\n}\n"],"names":["registrableDomain","normalizeSendingDomain","SENDING_SUBDOMAIN","sendingSpfInclude","parseSpf","records","terms","found","map","entry","String","trim","filter","test","length","record","count","includes","redirect","all","split","slice","term","toLowerCase","indexOf","replace","find","startsWith","allTerm","spfNamesInclude","policy","include","wanted","DMARC_POLICIES","parseDmarcRecord","policyOf","tags","subdomainPolicy","adkim","aspf","rua","pct","Map","part","at","set","value","lowered","mode","Number","get","Boolean","isFinite","Math","min","max","dmarcDomainsAlign","a","b","left","right","senderReadinessHosts","expectation","from","fromDomain","organizational","spf","envelopeDomain","dkim","dkimSelector","dkimDomain","dmarc","organizationalDmarc","resendSenderExpectation","domain","provider","spfInclude","GOOGLE_CONSUMER_DOMAINS","Set","googleMailboxSenderExpectation","address","raw","lastIndexOf","has","STATE_RANK","pass","unknown","warn","fail","worstSenderReadinessState","states","reduce","worst","state","assessSpf","host","txt","authorizes","detail","base","assessDkim","selector","keys","live","assessDmarcRecord","hosts","observation","dmarcTxt","check","policyDomain","parsed","inherited","organizationalDmarcTxt","inheritedNote","reports","join","assessSenderReadiness","nowMs","spfTxt","dkimTxt","dkimAligned","spfAligned","enforced","alignment","strictMiss","overall","checkedAtMs"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/sender-readiness.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { registrableDomain } from './outbound-phishing-screen'\nimport {\n normalizeSendingDomain,\n SENDING_SUBDOMAIN,\n sendingSpfInclude,\n} from './sending-domain'\n\n/*==========================================\n * SENDER READINESS — SPF, DKIM, DMARC and whether they line up.\n *\n * The recipient's side of a send is the deliverability engine's\n * (`email-deliverability.ts`); this is the sender's. A receiver decides\n * whether mail from a domain is really that domain's by three records the\n * SENDER publishes:\n *\n * - SPF, on the envelope (Return-Path) domain, naming the servers that may\n * send for it;\n * - DKIM, a public key at `<selector>._domainkey.<domain>`, which the\n * signature's `d=` names;\n * - DMARC, on the From domain (or its organizational domain), which says\n * what to do with mail that fails, and which only passes when SPF or\n * DKIM passes FOR A DOMAIN THAT ALIGNS WITH THE FROM DOMAIN.\n *\n * Alignment is the part a records table cannot show: every record can be\n * present and the mail can still fail DMARC, because the domain that passed\n * is not the one in the From line. So the verdict here is per record and\n * then aligned — relaxed alignment is \"same organizational domain\", strict\n * is \"the same name\", as `adkim=` / `aspf=` ask.\n *\n * Pure: the lookups are the server layer's (`sender-readiness.ts` in\n * `@aglyn/tenant-data-admin`), handed in as TXT answers, `null` for a lookup\n * nobody answered. An unanswered lookup is `unknown`, never `fail` — a\n * resolver outage must not tell a customer to rebuild a working zone.\n *\n * The sending-domain verification (`assessSendingRecords`) already decides\n * whether a Resend domain may SEND, on exact records. This reads the same\n * zone for a different question — will receivers believe it — and never\n * changes a domain's status.\n *==========================================*/\n\n/** How a check came out. `unknown` is a lookup nobody answered. */\nexport type SenderReadinessState = 'pass' | 'warn' | 'fail' | 'unknown'\n\n/** The `all` mechanism an SPF record ends with. */\nexport type SpfAllQualifier = '-all' | '~all' | '?all' | '+all'\n\n/** An SPF record, read. */\nexport interface SpfPolicy {\n /** The `v=spf1` record, or null when there is none (or more than one). */\n record: string | null\n /** How many `v=spf1` records the name publishes; more than one is a permanent error. */\n count: number\n /** Every `include:` target, lowercased. */\n includes: string[]\n /** The `redirect=` target, or null. */\n redirect: string | null\n /** The closing `all`, or null when the record has none. */\n all: SpfAllQualifier | null\n}\n\n/** Read a TXT answer as an SPF policy. Only `v=spf1` records count. */\nexport function parseSpf(records: readonly string[] | null | undefined): SpfPolicy {\n const found = (records ?? [])\n .map((entry) => String(entry ?? '').trim())\n .filter((entry) => /^v\\s*=\\s*spf1(\\s|$)/i.test(entry))\n if (found.length !== 1) {\n return { record: null, count: found.length, includes: [], redirect: null, all: null }\n }\n const record = found[0]\n const terms = record.split(/\\s+/).slice(1).map((term) => term.toLowerCase())\n const includes = terms\n .filter((term) => /^[+?~-]?include:/.test(term))\n .map((term) => term.slice(term.indexOf(':') + 1).replace(/\\.$/, ''))\n const redirect = terms.find((term) => term.startsWith('redirect='))?.slice('redirect='.length).replace(/\\.$/, '') ?? null\n const allTerm = terms.find((term) => /^[+?~-]?all$/.test(term)) ?? null\n const all = allTerm ? ((allTerm === 'all' ? '+all' : allTerm) as SpfAllQualifier) : null\n return { record, count: 1, includes, redirect, all }\n}\n\n/** Whether an SPF policy names `include` directly, as an include or its redirect. */\nexport function spfNamesInclude(policy: SpfPolicy, include: string): boolean {\n const wanted = String(include ?? '').trim().toLowerCase().replace(/\\.$/, '')\n if (!wanted) return false\n return policy.includes.includes(wanted) || policy.redirect === wanted\n}\n\n/** DMARC alignment mode: relaxed (same organizational domain) or strict (same name). */\nexport type DmarcAlignmentMode = 'r' | 's'\n\n/** A DMARC record, read. */\nexport interface DmarcRecordPolicy {\n record: string | null\n /** `p=`; `absent` when no record applies. */\n policy: 'reject' | 'quarantine' | 'none' | 'absent'\n /** `sp=`, the policy for subdomains, or null when the record sets none. */\n subdomainPolicy: 'reject' | 'quarantine' | 'none' | null\n adkim: DmarcAlignmentMode\n aspf: DmarcAlignmentMode\n /** `rua=` addresses, the aggregate reports' destinations. */\n rua: string[]\n /** `pct=`, 100 when unset. */\n pct: number\n}\n\nconst DMARC_POLICIES = ['reject', 'quarantine', 'none'] as const\ntype DmarcPolicyValue = (typeof DMARC_POLICIES)[number]\n\n/**\n * Read a `_dmarc` TXT answer. Only a record that begins `v=DMARC1` counts, as\n * in `assessDmarc`; tags are read case-insensitively and a tag it does not\n * know is ignored.\n */\nexport function parseDmarcRecord(records: readonly string[] | null | undefined): DmarcRecordPolicy {\n const found = (records ?? [])\n .map((entry) => String(entry ?? '').trim())\n .find((entry) => /^v\\s*=\\s*DMARC1\\b/i.test(entry))\n if (!found) {\n return { record: null, policy: 'absent', subdomainPolicy: null, adkim: 'r', aspf: 'r', rua: [], pct: 100 }\n }\n const tags = new Map<string, string>()\n for (const part of found.split(';')) {\n const at = part.indexOf('=')\n if (at < 0) continue\n tags.set(part.slice(0, at).trim().toLowerCase(), part.slice(at + 1).trim())\n }\n const policyOf = (value: string | undefined): DmarcPolicyValue | null => {\n const lowered = String(value ?? '').toLowerCase()\n return (DMARC_POLICIES as readonly string[]).includes(lowered) ? (lowered as DmarcPolicyValue) : null\n }\n const mode = (value: string | undefined): DmarcAlignmentMode => (String(value ?? '').toLowerCase() === 's' ? 's' : 'r')\n const pct = Number(tags.get('pct'))\n return {\n record: found,\n // A record with an unreadable `p=` is treated as `none`, which is what a\n // receiver does with it.\n policy: policyOf(tags.get('p')) ?? 'none',\n subdomainPolicy: policyOf(tags.get('sp')),\n adkim: mode(tags.get('adkim')),\n aspf: mode(tags.get('aspf')),\n rua: String(tags.get('rua') ?? '')\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean),\n pct: Number.isFinite(pct) ? Math.min(100, Math.max(0, pct)) : 100,\n }\n}\n\n/** Whether two domains align under a DMARC mode. */\nexport function dmarcDomainsAlign(a: string, b: string, mode: DmarcAlignmentMode): boolean {\n const left = normalizeSendingDomain(a)\n const right = normalizeSendingDomain(b)\n if (!left || !right) return false\n if (mode === 's') return left === right\n return registrableDomain(left) === registrableDomain(right)\n}\n\n/**\n * What a sender publishes for one provider to send as one From domain: where\n * each record is expected, and what it must name.\n */\nexport interface SenderReadinessExpectation {\n /** Who sends the mail, for the sentences. */\n provider: string\n /** The From domain. */\n fromDomain: string\n /** The envelope (Return-Path) domain, whose SPF a receiver checks. */\n envelopeDomain: string\n /** The `include:` the envelope domain's SPF must carry for this provider. */\n spfInclude: string\n /** The selector the provider signs with. */\n dkimSelector: string\n /** The signature's `d=`, where the key is published. */\n dkimDomain: string\n}\n\n/** A readiness expectation's DNS names, for the server layer to look up. */\nexport function senderReadinessHosts(expectation: SenderReadinessExpectation): {\n spf: string\n dkim: string\n dmarc: string\n organizationalDmarc: string | null\n} {\n const from = normalizeSendingDomain(expectation.fromDomain)\n const organizational = registrableDomain(from)\n return {\n spf: normalizeSendingDomain(expectation.envelopeDomain),\n dkim: `${expectation.dkimSelector}._domainkey.${normalizeSendingDomain(expectation.dkimDomain)}`,\n dmarc: `_dmarc.${from}`,\n organizationalDmarc: organizational && organizational !== from ? `_dmarc.${organizational}` : null,\n }\n}\n\n/**\n * A Resend sending domain's expectation: mail From `<domain>`, its envelope\n * on `send.<domain>` (the subdomain the records table asks for), signed\n * `d=<domain>` with the domain's issued selector.\n */\nexport function resendSenderExpectation(record: {\n domain: string\n dkimSelector?: string | null\n}): SenderReadinessExpectation | null {\n const domain = normalizeSendingDomain(record?.domain ?? '')\n if (!domain) return null\n return {\n provider: 'the mail provider',\n fromDomain: domain,\n envelopeDomain: `${SENDING_SUBDOMAIN}.${domain}`,\n spfInclude: sendingSpfInclude(),\n dkimSelector: String(record?.dkimSelector ?? '').trim() || 'aglyn',\n dkimDomain: domain,\n }\n}\n\n/**\n * Domains Google itself authenticates. A consumer Gmail address has nothing\n * for its owner to publish, so it has no readiness to show.\n */\nconst GOOGLE_CONSUMER_DOMAINS: ReadonlySet<string> = new Set(['gmail.com', 'googlemail.com'])\n\n/**\n * A Google Workspace mailbox's expectation: mail From the address's domain,\n * sent through Google with the envelope on the same domain, SPF naming\n * `_spf.google.com` and the key under Google's default `google` selector.\n * `null` for a consumer Gmail address or an unreadable one.\n */\nexport function googleMailboxSenderExpectation(address: string | null | undefined): SenderReadinessExpectation | null {\n const raw = String(address ?? '').trim().toLowerCase()\n const domain = normalizeSendingDomain(raw.slice(raw.lastIndexOf('@') + 1))\n if (!raw.includes('@') || !domain || GOOGLE_CONSUMER_DOMAINS.has(domain)) return null\n return {\n provider: 'Google Workspace',\n fromDomain: domain,\n envelopeDomain: domain,\n spfInclude: '_spf.google.com',\n dkimSelector: 'google',\n dkimDomain: domain,\n }\n}\n\n/** Microsoft's consumer domains, and the tenant domains it names itself. */\nconst MICROSOFT_CONSUMER_DOMAINS: ReadonlySet<string> = new Set(['outlook.com', 'hotmail.com', 'live.com', 'msn.com'])\n\n/**\n * A Microsoft 365 mailbox's expectation (AGL-3489): mail From the address's\n * domain, sent through Exchange Online with the envelope on the same domain,\n * SPF naming `spf.protection.outlook.com`, and the key under `selector1` —\n * the CNAME Microsoft 365 publishes DKIM through, which a TXT lookup\n * follows. `null` for a consumer Outlook address, an `onmicrosoft.com`\n * tenant address, or an unreadable one: their records are Microsoft's own.\n */\nexport function microsoftMailboxSenderExpectation(\n address: string | null | undefined,\n): SenderReadinessExpectation | null {\n const raw = String(address ?? '').trim().toLowerCase()\n const domain = normalizeSendingDomain(raw.slice(raw.lastIndexOf('@') + 1))\n if (!raw.includes('@') || !domain || MICROSOFT_CONSUMER_DOMAINS.has(domain) || domain.endsWith('.onmicrosoft.com')) {\n return null\n }\n return {\n provider: 'Microsoft 365',\n fromDomain: domain,\n envelopeDomain: domain,\n spfInclude: 'spf.protection.outlook.com',\n dkimSelector: 'selector1',\n dkimDomain: domain,\n }\n}\n\n/** The TXT answers a readiness read needs; `null` for a lookup nobody answered. */\nexport interface SenderReadinessObservation {\n spfTxt: readonly string[] | null\n dkimTxt: readonly string[] | null\n dmarcTxt: readonly string[] | null\n /**\n * `_dmarc` at the organizational domain, asked only when the From domain\n * is a subdomain and publishes no record of its own. `undefined` when it\n * was not asked.\n */\n organizationalDmarcTxt?: readonly string[] | null\n}\n\n/** One check, with the sentence a card shows for it. */\nexport interface SenderReadinessCheck {\n state: SenderReadinessState\n /** Where the record was looked for. */\n host: string\n /** The record as published, when there is one. */\n record: string | null\n detail: string\n}\n\nexport interface SenderReadiness {\n provider: string\n fromDomain: string\n spf: SenderReadinessCheck & { all: SpfAllQualifier | null; authorizes: boolean }\n dkim: SenderReadinessCheck & { selector: string }\n dmarc: SenderReadinessCheck & {\n policy: DmarcRecordPolicy['policy']\n rua: string[]\n /** The domain whose record applies — the From domain's, or its organizational domain's. */\n policyDomain: string\n }\n /** Whether DMARC passes, and on which mechanism. */\n alignment: SenderReadinessCheck & { spfAligned: boolean | null; dkimAligned: boolean | null }\n /** The worst of the four. */\n overall: SenderReadinessState\n checkedAtMs: number\n}\n\nconst STATE_RANK: Record<SenderReadinessState, number> = { pass: 0, unknown: 1, warn: 2, fail: 3 }\n\n/** The worst of several states: fail over warn over unknown over pass. */\nexport function worstSenderReadinessState(states: readonly SenderReadinessState[]): SenderReadinessState {\n return states.reduce<SenderReadinessState>((worst, state) => (STATE_RANK[state] > STATE_RANK[worst] ? state : worst), 'pass')\n}\n\nfunction assessSpf(expectation: SenderReadinessExpectation, host: string, txt: readonly string[] | null): SenderReadiness['spf'] {\n if (!txt) {\n return { state: 'unknown', host, record: null, all: null, authorizes: false, detail: `The SPF lookup for ${host} went unanswered.` }\n }\n const policy = parseSpf(txt)\n if (policy.count > 1) {\n return {\n state: 'fail',\n host,\n record: null,\n all: null,\n authorizes: false,\n detail: `${host} publishes ${policy.count} SPF records. Receivers treat more than one as an error and SPF fails for every message; merge them into one.`,\n }\n }\n if (!policy.record) {\n return {\n state: 'fail',\n host,\n record: null,\n all: null,\n authorizes: false,\n detail: `${host} publishes no SPF record, so no receiver can tell ${expectation.provider} is allowed to send for it.`,\n }\n }\n const authorizes = spfNamesInclude(policy, expectation.spfInclude)\n const base = { host, record: policy.record, all: policy.all, authorizes }\n if (policy.all === '+all') {\n return {\n ...base,\n state: 'fail',\n detail: 'The record ends in +all, which authorizes every server on the internet to send as this domain.',\n }\n }\n if (!authorizes) {\n return {\n ...base,\n state: 'warn',\n detail:\n `The record does not name include:${expectation.spfInclude}. If it reaches it through another ` +\n `include this is fine; otherwise mail sent by ${expectation.provider} fails SPF.`,\n }\n }\n if (policy.all === '-all') {\n return { ...base, state: 'pass', detail: `Authorizes ${expectation.provider} and refuses every other server (-all).` }\n }\n if (policy.all === '~all') {\n return {\n ...base,\n state: 'pass',\n detail: `Authorizes ${expectation.provider}; other servers soft-fail (~all), the usual setting while DMARC does the enforcing.`,\n }\n }\n if (!policy.all && policy.redirect) {\n return { ...base, state: 'pass', detail: `Authorizes ${expectation.provider} through redirect=${policy.redirect}.` }\n }\n return {\n ...base,\n state: 'warn',\n detail:\n policy.all === '?all'\n ? `Authorizes ${expectation.provider}, but ?all says nothing about every other server. End it in ~all or -all.`\n : `Authorizes ${expectation.provider}, but the record has no closing all, which reads as ?all. End it in ~all or -all.`,\n }\n}\n\nfunction assessDkim(expectation: SenderReadinessExpectation, host: string, txt: readonly string[] | null): SenderReadiness['dkim'] {\n const selector = expectation.dkimSelector\n if (!txt) return { state: 'unknown', host, record: null, selector, detail: `The DKIM lookup for ${host} went unanswered.` }\n const keys = txt.map((entry) => String(entry ?? '').trim()).filter((entry) => /(^|;)\\s*p\\s*=/i.test(entry))\n const live = keys.find((entry) => /(^|;)\\s*p\\s*=\\s*[A-Za-z0-9+/]/.test(entry))\n if (live) {\n return { state: 'pass', host, record: live, selector, detail: `A signing key is published for the ${selector} selector.` }\n }\n if (keys.length) {\n return {\n state: 'fail',\n host,\n record: keys[0],\n selector,\n detail: `The ${selector} selector's key is empty (p=), which revokes it. Mail signed with it fails DKIM.`,\n }\n }\n return {\n state: 'fail',\n host,\n record: null,\n selector,\n detail: `No DKIM key is published for the ${selector} selector, so ${expectation.provider}'s signature cannot be checked.`,\n }\n}\n\nfunction assessDmarcRecord(\n expectation: SenderReadinessExpectation,\n hosts: ReturnType<typeof senderReadinessHosts>,\n observation: SenderReadinessObservation,\n): { check: SenderReadiness['dmarc']; policy: DmarcRecordPolicy | null } {\n const from = normalizeSendingDomain(expectation.fromDomain)\n if (!observation.dmarcTxt) {\n return {\n check: {\n state: 'unknown',\n host: hosts.dmarc,\n record: null,\n policy: 'absent',\n rua: [],\n policyDomain: from,\n detail: `The DMARC lookup for ${hosts.dmarc} went unanswered.`,\n },\n policy: null,\n }\n }\n let parsed = parseDmarcRecord(observation.dmarcTxt)\n let host = hosts.dmarc\n let policyDomain = from\n let inherited = false\n if (!parsed.record && hosts.organizationalDmarc) {\n if (observation.organizationalDmarcTxt === null) {\n return {\n check: {\n state: 'unknown',\n host: hosts.organizationalDmarc,\n record: null,\n policy: 'absent',\n rua: [],\n policyDomain: registrableDomain(from),\n detail: `The DMARC lookup for ${hosts.organizationalDmarc} went unanswered.`,\n },\n policy: null,\n }\n }\n const organizational = parseDmarcRecord(observation.organizationalDmarcTxt ?? [])\n if (organizational.record) {\n // A subdomain with no record of its own takes the organizational\n // domain's, under its `sp=` when it sets one.\n parsed = { ...organizational, policy: organizational.subdomainPolicy ?? organizational.policy }\n host = hosts.organizationalDmarc\n policyDomain = registrableDomain(from)\n inherited = true\n }\n }\n const base = { host, record: parsed.record, policy: parsed.policy, rua: parsed.rua, policyDomain }\n if (!parsed.record) {\n return {\n check: {\n ...base,\n state: 'warn',\n detail:\n `${from} publishes no DMARC policy. Gmail and Yahoo expect one from anyone sending in bulk, ` +\n 'and without it anyone can send mail claiming to be from this domain.',\n },\n policy: parsed,\n }\n }\n const inheritedNote = inherited ? ` (inherited from ${policyDomain})` : ''\n const reports = parsed.rua.length\n ? ` Aggregate reports go to ${parsed.rua.join(', ')}.`\n : ' It names no rua address, so no aggregate reports reach anyone.'\n if (parsed.policy === 'none') {\n return {\n check: {\n ...base,\n state: parsed.rua.length ? 'pass' : 'warn',\n detail: `p=none${inheritedNote}: DMARC monitors and enforces nothing.${reports}`,\n },\n policy: parsed,\n }\n }\n return {\n check: {\n ...base,\n state: 'pass',\n detail: `p=${parsed.policy}${parsed.pct < 100 ? ` at pct=${parsed.pct}` : ''}${inheritedNote}: mail that fails DMARC is ${\n parsed.policy === 'reject' ? 'refused' : 'sent to spam'\n }.${reports}`,\n },\n policy: parsed,\n }\n}\n\n/**\n * Whether one provider's mail as one From domain will be believed: each\n * record, and then whether DMARC passes on a mechanism that aligns.\n */\nexport function assessSenderReadiness(\n expectation: SenderReadinessExpectation,\n observation: SenderReadinessObservation,\n nowMs: number,\n): SenderReadiness {\n const hosts = senderReadinessHosts(expectation)\n const spf = assessSpf(expectation, hosts.spf, observation.spfTxt)\n const dkim = assessDkim(expectation, hosts.dkim, observation.dkimTxt)\n const { check: dmarc, policy } = assessDmarcRecord(expectation, hosts, observation)\n\n const adkim = policy?.adkim ?? 'r'\n const aspf = policy?.aspf ?? 'r'\n const dkimAligned =\n dkim.state === 'unknown' ? null : dkim.state === 'pass' && dmarcDomainsAlign(expectation.dkimDomain, expectation.fromDomain, adkim)\n const spfAligned =\n spf.state === 'unknown'\n ? null\n : spf.authorizes && spf.state !== 'fail' && dmarcDomainsAlign(expectation.envelopeDomain, expectation.fromDomain, aspf)\n const enforced = policy?.policy === 'reject' || policy?.policy === 'quarantine'\n let alignment: SenderReadiness['alignment']\n const host = normalizeSendingDomain(expectation.fromDomain)\n if (dkimAligned) {\n alignment = {\n state: 'pass',\n host,\n record: null,\n spfAligned,\n dkimAligned,\n detail: `DMARC passes on DKIM: the signature's domain lines up with the From domain${\n adkim === 's' ? ' exactly (adkim=s)' : ''\n }, and a signature survives forwarding.`,\n }\n } else if (spfAligned) {\n alignment = {\n state: 'warn',\n host,\n record: null,\n spfAligned,\n dkimAligned,\n detail:\n 'DMARC passes on SPF alone. Forwarding breaks SPF, so forwarded mail fails DMARC until the DKIM key is published.',\n }\n } else if (dkimAligned === null || spfAligned === null) {\n alignment = {\n state: 'unknown',\n host,\n record: null,\n spfAligned,\n dkimAligned,\n detail: 'Alignment cannot be judged until the lookups above are answered.',\n }\n } else {\n const strictMiss =\n (adkim === 's' && dkim.state === 'pass') || (aspf === 's' && spf.authorizes)\n ? ' The DMARC record asks for strict alignment, which only an exact name satisfies.'\n : ''\n alignment = {\n state: 'fail',\n host,\n record: null,\n spfAligned,\n dkimAligned,\n detail: `Neither SPF nor DKIM passes for a domain that lines up with ${host}, so DMARC fails${\n enforced ? ` and p=${policy?.policy} ${policy?.policy === 'reject' ? 'refuses' : 'quarantines'} the mail` : ''\n }.${strictMiss}`,\n }\n }\n\n return {\n provider: expectation.provider,\n fromDomain: host,\n spf,\n dkim,\n dmarc,\n alignment,\n overall: worstSenderReadinessState([spf.state, dkim.state, dmarc.state, alignment.state]),\n checkedAtMs: nowMs,\n }\n}\n"],"names":["registrableDomain","normalizeSendingDomain","SENDING_SUBDOMAIN","sendingSpfInclude","parseSpf","records","terms","found","map","entry","String","trim","filter","test","length","record","count","includes","redirect","all","split","slice","term","toLowerCase","indexOf","replace","find","startsWith","allTerm","spfNamesInclude","policy","include","wanted","DMARC_POLICIES","parseDmarcRecord","policyOf","tags","subdomainPolicy","adkim","aspf","rua","pct","Map","part","at","set","value","lowered","mode","Number","get","Boolean","isFinite","Math","min","max","dmarcDomainsAlign","a","b","left","right","senderReadinessHosts","expectation","from","fromDomain","organizational","spf","envelopeDomain","dkim","dkimSelector","dkimDomain","dmarc","organizationalDmarc","resendSenderExpectation","domain","provider","spfInclude","GOOGLE_CONSUMER_DOMAINS","Set","googleMailboxSenderExpectation","address","raw","lastIndexOf","has","MICROSOFT_CONSUMER_DOMAINS","microsoftMailboxSenderExpectation","endsWith","STATE_RANK","pass","unknown","warn","fail","worstSenderReadinessState","states","reduce","worst","state","assessSpf","host","txt","authorizes","detail","base","assessDkim","selector","keys","live","assessDmarcRecord","hosts","observation","dmarcTxt","check","policyDomain","parsed","inherited","organizationalDmarcTxt","inheritedNote","reports","join","assessSenderReadiness","nowMs","spfTxt","dkimTxt","dkimAligned","spfAligned","enforced","alignment","strictMiss","overall","checkedAtMs"],"mappings":"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"}