mailauth 5.0.3 → 7.0.0
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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +23 -0
- package/README.md +117 -41
- package/bin/mailauth.js +14 -6
- package/cli.md +8 -3
- package/docs/arc.md +126 -43
- package/docs/bimi.md +54 -37
- package/docs/dkim.md +79 -40
- package/docs/dmarc.md +82 -31
- package/docs/mta-sts.md +140 -42
- package/docs/spf.md +69 -4
- package/index.d.ts +499 -45
- package/lib/arc/index.d.ts +10 -8
- package/lib/arc/index.js +505 -124
- package/lib/bimi/index.js +162 -81
- package/lib/commands/report.js +2 -1
- package/lib/commands/seal.js +35 -5
- package/lib/commands/sign.js +8 -1
- package/lib/commands/spf.js +8 -0
- package/lib/dkim/body/relaxed.js +6 -5
- package/lib/dkim/body/simple.js +1 -3
- package/lib/dkim/dkim-signer.js +219 -33
- package/lib/dkim/dkim-verifier.js +574 -231
- package/lib/dkim/header/relaxed.js +6 -43
- package/lib/dkim/header/signature-opts.js +87 -0
- package/lib/dkim/header/simple.js +6 -43
- package/lib/dkim/message-parser.js +18 -4
- package/lib/dkim/sign.d.ts +13 -7
- package/lib/dkim/sign.js +18 -2
- package/lib/dkim/syntax.js +31 -0
- package/lib/dkim/verify.js +1 -0
- package/lib/dmarc/author-domain.js +181 -0
- package/lib/dmarc/get-dmarc-record.js +267 -53
- package/lib/dmarc/index.d.ts +29 -2
- package/lib/dmarc/index.js +2 -1
- package/lib/dmarc/verify.js +205 -53
- package/lib/mailauth.js +86 -40
- package/lib/mta-sts.d.ts +77 -11
- package/lib/mta-sts.js +497 -182
- package/lib/parse-dkim-headers.js +376 -191
- package/lib/parse-received.js +104 -32
- package/lib/spf/index.d.ts +37 -1
- package/lib/spf/index.js +260 -28
- package/lib/spf/macro.js +50 -13
- package/lib/spf/spf-verify.js +440 -204
- package/lib/spf/syntax.js +366 -0
- package/lib/tools.js +524 -118
- package/package.json +12 -12
|
@@ -1,86 +1,300 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const dns = require('node:dns').promises;
|
|
4
|
-
const
|
|
5
|
-
const {
|
|
4
|
+
const net = require('node:net');
|
|
5
|
+
const { formatDomain } = require('../tools');
|
|
6
6
|
|
|
7
7
|
// Tags whose values are ABNF literals in RFC 7489 6.4 / RFC 9989 4.7 and are therefore
|
|
8
8
|
// case insensitive (RFC 5234 2.3). Values of other tags keep their case: "v" is defined
|
|
9
9
|
// with explicit hex literals and is case sensitive, "rua"/"ruf" are URIs.
|
|
10
10
|
const CASE_INSENSITIVE_TAGS = new Set(['p', 'sp', 'np', 'adkim', 'aspf', 'fo', 'rf', 't', 'psd']);
|
|
11
11
|
|
|
12
|
-
// Returns every DMARC record published for a domain. The caller distinguishes an empty set,
|
|
13
|
-
// which continues policy discovery at the organizational domain, from a multi record set,
|
|
14
|
-
// which terminates it (RFC 7489 6.6.3 step 5).
|
|
15
12
|
// The version tag must be the first tag and its value "DMARC1" must match precisely
|
|
16
13
|
// (RFC 9989 4.7 "v"), while the ABNF allows *WSP around "=" and a case insensitive
|
|
17
14
|
// tag name, hence [vV] instead of an /i flag that would also relax the value
|
|
18
15
|
const DMARC_VERSION_RE = /^[vV][ \t]*=[ \t]*DMARC1[ \t]*(?:;|$)/;
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
// Resolver errors that mean the name has no record, as opposed to a transient failure.
|
|
18
|
+
// EBADNAME is a name that can not exist in the DNS at all.
|
|
19
|
+
const NO_RECORD_CODES = new Set(['ENOTFOUND', 'ENODATA', 'EBADNAME']);
|
|
20
|
+
|
|
21
|
+
// A Tree Walk shortens an author domain of eight or more labels straight to seven (RFC 9989 4.10 step 4)
|
|
22
|
+
const WALK_MAX_LABELS = 7;
|
|
23
|
+
|
|
24
|
+
// A domain literal or an IP address is not a DNS name and can not publish a DMARC record
|
|
25
|
+
const isAddress = domain => domain.startsWith('[') || net.isIP(domain) !== 0;
|
|
26
|
+
|
|
27
|
+
// Whether a name can exist in the DNS: no empty labels, labels of at most 63 octets and a
|
|
28
|
+
// name of at most 253 octets (RFC 1035 2.3.4). Anything else would only produce a resolver
|
|
29
|
+
// error that looks like a transient failure.
|
|
30
|
+
const isValidName = name => name.length <= 253 && name.split('.').every(label => label.length && label.length <= 63);
|
|
31
|
+
|
|
32
|
+
const POLICY_VALUES = new Set(['none', 'quarantine', 'reject']);
|
|
33
|
+
|
|
34
|
+
// A reporting URI (RFC 9989 4.8 dmarc-uri): an RFC 3986 URI, so a scheme, a ":" and a possibly
|
|
35
|
+
// empty rest made of URI characters, where commas and "!" must be percent-encoded. "mailto:" is
|
|
36
|
+
// therefore valid and a bare address is not. An obsolete "!size" suffix does not change that.
|
|
37
|
+
const REPORTING_URI = /^[a-z][a-z0-9+.-]*:(?:[a-z0-9\-._~:/?#[\]@$&'()*+;=]|%[0-9a-f]{2})*$/i;
|
|
38
|
+
const OBS_REPORT_SIZE = /![0-9]+[kmgt]?$/i;
|
|
39
|
+
|
|
40
|
+
// "rua" lists at least one syntactically valid reporting URI (RFC 9989 4.10.1)
|
|
41
|
+
const hasReportingUri = record =>
|
|
42
|
+
typeof record.rua === 'string' && record.rua.split(',').some(uri => REPORTING_URI.test(uri.trim().replace(OBS_REPORT_SIZE, '')));
|
|
43
|
+
|
|
44
|
+
// Returns the policy tags a record is applied with (RFC 9989 4.10.1). A record without a
|
|
45
|
+
// valid "p", or with an invalid "sp" or "np", acts as p=none when it asks for aggregate
|
|
46
|
+
// reports, and gets no DMARC processing at all (null) otherwise.
|
|
47
|
+
const getPolicyTags = record => {
|
|
48
|
+
const isValid = value => value === undefined || POLICY_VALUES.has(value);
|
|
49
|
+
if (POLICY_VALUES.has(record.p) && isValid(record.sp) && isValid(record.np)) {
|
|
50
|
+
return { p: record.p, sp: record.sp, np: record.np };
|
|
51
|
+
}
|
|
52
|
+
return hasReportingUri(record) ? { p: 'none' } : null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Domain existence test (RFC 9989 A.4): any RR makes a name exist, only NXDOMAIN means it
|
|
56
|
+
// does not. A query for A tells the two apart, NODATA is an existing name without A records.
|
|
57
|
+
const domainExists = async (domain, resolver) => {
|
|
58
|
+
if (!isValidName(domain)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
21
61
|
try {
|
|
22
|
-
|
|
23
|
-
return
|
|
62
|
+
await (resolver || dns.resolve)(domain, 'A');
|
|
63
|
+
return true;
|
|
24
64
|
} catch (err) {
|
|
25
|
-
if (err.code === '
|
|
26
|
-
return
|
|
65
|
+
if (err.code === 'ENODATA') {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (err.code === 'ENOTFOUND' || err.code === 'EBADNAME') {
|
|
69
|
+
return false;
|
|
27
70
|
}
|
|
28
71
|
throw err;
|
|
29
72
|
}
|
|
30
73
|
};
|
|
31
74
|
|
|
32
|
-
|
|
33
|
-
|
|
75
|
+
// Tag names registered for DMARC (RFC 9989 9.3), including the historic ones
|
|
76
|
+
const KNOWN_TAGS = new Set(['v', 'p', 'sp', 'np', 'adkim', 'aspf', 'fo', 'rua', 'ruf', 'rf', 'ri', 'pct', 't', 'psd']);
|
|
77
|
+
|
|
78
|
+
// Parses a DMARC Policy Record. DMARC uses the DKIM tag-list syntax (RFC 9989 4.7), where tag names
|
|
79
|
+
// are case sensitive and a duplicated tag makes the whole tag-list invalid (RFC 6376 3.2).
|
|
80
|
+
// By default tag names are case-folded and the last of duplicated tags wins, which is reported in
|
|
81
|
+
// "warnings". With "strict" the RFC rules apply and a record with a duplicated tag is invalid (null).
|
|
82
|
+
const parseDmarcRecord = (txt, opts) => {
|
|
83
|
+
const { strict, warnings } = opts || {};
|
|
84
|
+
|
|
85
|
+
let seen = new Set();
|
|
86
|
+
let duplicate = false;
|
|
87
|
+
|
|
88
|
+
let entries = txt
|
|
89
|
+
.split(';')
|
|
90
|
+
.map(e => e.trim())
|
|
91
|
+
.filter(e => e)
|
|
92
|
+
.map((e, i) => {
|
|
93
|
+
let splitPos = e.indexOf('=');
|
|
94
|
+
if (splitPos < 0) {
|
|
95
|
+
// not a tag, a syntax error that is ignored
|
|
96
|
+
return [e.toLowerCase().trim(), false];
|
|
97
|
+
} else if (splitPos === 0) {
|
|
98
|
+
return [false, e];
|
|
99
|
+
}
|
|
100
|
+
// whitespace is allowed around "=" (*WSP in the ABNF), so the value must be trimmed
|
|
101
|
+
let name = e.substr(0, splitPos).trim();
|
|
102
|
+
let key = name.toLowerCase();
|
|
103
|
+
if (strict && i > 0) {
|
|
104
|
+
// the version tag is matched case-insensitively by its own ABNF rule, other names are not
|
|
105
|
+
key = name;
|
|
106
|
+
} else if (key !== name && KNOWN_TAGS.has(key) && i > 0) {
|
|
107
|
+
warnings?.add('tag-case');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (seen.has(key)) {
|
|
111
|
+
duplicate = true;
|
|
112
|
+
}
|
|
113
|
+
seen.add(key);
|
|
114
|
+
|
|
115
|
+
let val = e.substr(splitPos + 1).trim();
|
|
116
|
+
if (['pct', 'ri'].includes(key)) {
|
|
117
|
+
val = parseInt(val, 10) || 0;
|
|
118
|
+
} else if (CASE_INSENSITIVE_TAGS.has(key)) {
|
|
119
|
+
val = val.toLowerCase();
|
|
120
|
+
}
|
|
121
|
+
return [key, val];
|
|
122
|
+
});
|
|
34
123
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!records.length) {
|
|
39
|
-
let orgDomain = tldts.getDomain(domain, TLDTS_OPTS);
|
|
40
|
-
// tldts returns null for IPs, single labels and other non-domains, and querying
|
|
41
|
-
// "_dmarc.null" would coerce that into a hostname
|
|
42
|
-
if (orgDomain && orgDomain !== domain) {
|
|
43
|
-
// try org domain as well
|
|
44
|
-
records = await resolveTxt(orgDomain, resolver);
|
|
45
|
-
isOrgRecord = true;
|
|
124
|
+
if (duplicate) {
|
|
125
|
+
if (strict) {
|
|
126
|
+
return null;
|
|
46
127
|
}
|
|
128
|
+
warnings?.add('duplicate-tag');
|
|
47
129
|
}
|
|
48
130
|
|
|
49
|
-
|
|
50
|
-
|
|
131
|
+
let parsed = Object.fromEntries(entries);
|
|
132
|
+
|
|
133
|
+
parsed.rr = txt;
|
|
134
|
+
|
|
135
|
+
return parsed;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Returns the DMARC Policy Record published at "_dmarc.<name>", or false. A set of several
|
|
139
|
+
// records counts as no record, it is discarded (RFC 9989 4.10 steps 2 and 6).
|
|
140
|
+
const fetchRecord = async (name, resolver, opts) => {
|
|
141
|
+
if (!isValidName(`_dmarc.${name}`)) {
|
|
51
142
|
return false;
|
|
52
143
|
}
|
|
53
144
|
|
|
54
|
-
let
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
let key = e.substr(0, splitPos).toLowerCase().trim();
|
|
70
|
-
let val = e.substr(splitPos + 1).trim();
|
|
71
|
-
if (['pct', 'ri'].includes(key)) {
|
|
72
|
-
val = parseInt(val, 10) || 0;
|
|
73
|
-
} else if (CASE_INSENSITIVE_TAGS.has(key)) {
|
|
74
|
-
val = val.toLowerCase();
|
|
75
|
-
}
|
|
76
|
-
return [key, val];
|
|
77
|
-
})
|
|
78
|
-
);
|
|
145
|
+
let records;
|
|
146
|
+
try {
|
|
147
|
+
let txt = await resolver(`_dmarc.${name}`, 'TXT');
|
|
148
|
+
// a custom resolver might return plain strings instead of lists of character-strings
|
|
149
|
+
records = (txt || []).map(row => (Array.isArray(row) ? row.join('') : String(row)).trim()).filter(row => DMARC_VERSION_RE.test(row));
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (NO_RECORD_CODES.has(err.code)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// a record that is not a valid tag-list is discarded like one that is not a DMARC record
|
|
158
|
+
return (records.length === 1 && parseDmarcRecord(records[0], opts)) || false;
|
|
159
|
+
};
|
|
79
160
|
|
|
80
|
-
|
|
81
|
-
|
|
161
|
+
// Tree Walks for one message share most of their names, so each name is queried only once.
|
|
162
|
+
// opts.strict parses records by the RFC 6376 tag-list rules, opts.warnings is a Set that collects
|
|
163
|
+
// what the default parsing accepted and strict parsing would not.
|
|
164
|
+
const createRecordLookup = (resolver, opts) => {
|
|
165
|
+
resolver = resolver || dns.resolve;
|
|
166
|
+
let cache = new Map();
|
|
167
|
+
return name => {
|
|
168
|
+
if (!cache.has(name)) {
|
|
169
|
+
cache.set(name, fetchRecord(name, resolver, opts));
|
|
170
|
+
}
|
|
171
|
+
return cache.get(name);
|
|
172
|
+
};
|
|
173
|
+
};
|
|
82
174
|
|
|
83
|
-
|
|
175
|
+
// DNS Tree Walk (RFC 9989 4.10). Returns the records found, longest name first. The walk
|
|
176
|
+
// stops at a record with a psd=y or psd=n tag, or when no labels are left, so it makes
|
|
177
|
+
// at most eight queries. A DNS error ends the walk and is returned next to the records
|
|
178
|
+
// found before it, because a record found at the start domain is usable without the rest.
|
|
179
|
+
const treeWalk = async (domain, lookup) => {
|
|
180
|
+
let found = [];
|
|
181
|
+
let labels = domain.split('.');
|
|
182
|
+
|
|
183
|
+
while (labels.length) {
|
|
184
|
+
let name = labels.join('.');
|
|
185
|
+
let record;
|
|
186
|
+
try {
|
|
187
|
+
record = await lookup(name);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
return { found, error: err };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (record) {
|
|
193
|
+
found.push({ domain: name, record });
|
|
194
|
+
if (record.psd === 'y' || record.psd === 'n') {
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
labels = labels.length > WALK_MAX_LABELS ? labels.slice(-WALK_MAX_LABELS) : labels.slice(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { found };
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Organizational Domain selection for a completed Tree Walk (RFC 9989 4.10.2)
|
|
206
|
+
const selectOrgDomain = (domain, found) => {
|
|
207
|
+
for (let { domain: recordDomain, record } of found) {
|
|
208
|
+
if (record.psd === 'n') {
|
|
209
|
+
return recordDomain;
|
|
210
|
+
}
|
|
211
|
+
if (record.psd === 'y' && recordDomain !== domain) {
|
|
212
|
+
// the domain one label below the PSD
|
|
213
|
+
let labelCount = recordDomain.split('.').length + 1;
|
|
214
|
+
return domain.split('.').slice(-labelCount).join('.');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// the record with the fewest labels, or the start domain itself when nothing was found
|
|
219
|
+
return found.length ? found[found.length - 1].domain : domain;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Returns the Organizational Domain of a normalized domain name. Throws on DNS errors.
|
|
223
|
+
const getOrgDomain = async (domain, lookup) => {
|
|
224
|
+
if (isAddress(domain)) {
|
|
225
|
+
return domain;
|
|
226
|
+
}
|
|
227
|
+
let { found, error } = await treeWalk(domain, lookup);
|
|
228
|
+
if (error) {
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
return selectOrgDomain(domain, found);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// The record that holds the policy of an Organizational Domain: its own record, or else the record
|
|
235
|
+
// of the PSD above it. Returns false when there is neither.
|
|
236
|
+
const findOrgRecord = (orgDomain, found) => {
|
|
237
|
+
let last = found[found.length - 1];
|
|
238
|
+
let entry = found.find(e => e.domain === orgDomain) || (last?.record.psd === 'y' ? last : null);
|
|
239
|
+
return entry || false;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// DMARC Policy Discovery (RFC 9989 4.10.1) for a normalized author domain. Returns
|
|
243
|
+
// { record, recordDomain, orgDomain, orgRecord, error }, where "record" is false when no policy
|
|
244
|
+
// applies and "orgRecord" is the record that holds the Organizational Domain's policy (see findOrgRecord).
|
|
245
|
+
// When the Tree Walk fails after a record was found at the author domain, "orgDomain" and
|
|
246
|
+
// "orgRecord" are null and "error" says why. Throws on DNS errors that leave the policy unknown.
|
|
247
|
+
const discoverPolicy = async (domain, lookup) => {
|
|
248
|
+
if (isAddress(domain)) {
|
|
249
|
+
return { record: false, recordDomain: null, orgDomain: domain, orgRecord: false };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let { found, error } = await treeWalk(domain, lookup);
|
|
253
|
+
|
|
254
|
+
if (found[0]?.domain === domain) {
|
|
255
|
+
// the author domain's own record applies regardless of what is above it
|
|
256
|
+
if (error) {
|
|
257
|
+
return { record: found[0].record, recordDomain: domain, orgDomain: null, orgRecord: null, error };
|
|
258
|
+
}
|
|
259
|
+
let orgDomain = selectOrgDomain(domain, found);
|
|
260
|
+
return { record: found[0].record, recordDomain: domain, orgDomain, orgRecord: findOrgRecord(orgDomain, found).record || false };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (error) {
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let orgDomain = selectOrgDomain(domain, found);
|
|
268
|
+
|
|
269
|
+
// otherwise the record of the Organizational Domain, and failing that the record of the PSD.
|
|
270
|
+
// A walk only stops at a psd tag, so a psd=y record can only be the last one found.
|
|
271
|
+
let entry = findOrgRecord(orgDomain, found);
|
|
272
|
+
|
|
273
|
+
return { record: entry ? entry.record : false, recordDomain: entry ? entry.domain : null, orgDomain, orgRecord: entry ? entry.record : false };
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// Fetches the DMARC Policy Record that applies to a domain. Returns the parsed record or false.
|
|
277
|
+
// opts.strict parses records by the RFC 6376 tag-list rules (see parseDmarcRecord).
|
|
278
|
+
const getDmarcRecord = async (domain, resolver, opts) => {
|
|
279
|
+
domain = formatDomain(domain || '');
|
|
280
|
+
|
|
281
|
+
let { record, recordDomain, orgDomain } = await discoverPolicy(domain, createRecordLookup(resolver, { strict: !!opts?.strict }));
|
|
282
|
+
if (!record) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return Object.assign(record, {
|
|
287
|
+
// the record is inherited from the Organizational Domain or the PSD
|
|
288
|
+
isOrgRecord: recordDomain !== domain,
|
|
289
|
+
recordDomain,
|
|
290
|
+
orgDomain
|
|
291
|
+
});
|
|
84
292
|
};
|
|
85
293
|
|
|
86
294
|
module.exports = getDmarcRecord;
|
|
295
|
+
module.exports.createRecordLookup = createRecordLookup;
|
|
296
|
+
module.exports.discoverPolicy = discoverPolicy;
|
|
297
|
+
module.exports.getOrgDomain = getOrgDomain;
|
|
298
|
+
module.exports.getPolicyTags = getPolicyTags;
|
|
299
|
+
module.exports.domainExists = domainExists;
|
|
300
|
+
module.exports.parseDmarcRecord = parseDmarcRecord;
|
package/lib/dmarc/index.d.ts
CHANGED
|
@@ -6,6 +6,33 @@ import { DMARCOptions, DMARCResult } from '../../index';
|
|
|
6
6
|
* Verifies DMARC policy for a message
|
|
7
7
|
*
|
|
8
8
|
* @param opts - DMARC verification options
|
|
9
|
-
* @returns DMARC verification result
|
|
9
|
+
* @returns DMARC verification result, or false when the From header has no domain or more than one domain
|
|
10
10
|
*/
|
|
11
|
-
export function dmarc(opts: DMARCOptions): Promise<DMARCResult | false>;
|
|
11
|
+
export function dmarc(opts: DMARCOptions & { strict?: boolean }): Promise<DMARCResult | false>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Verifies DMARC policy for a message, with details that dmarc() does not return
|
|
15
|
+
*
|
|
16
|
+
* @param opts - DMARC verification options
|
|
17
|
+
*/
|
|
18
|
+
export function evaluateDmarc(opts: DMARCOptions & { strict?: boolean }): Promise<{
|
|
19
|
+
/**
|
|
20
|
+
* The dmarc() result
|
|
21
|
+
*/
|
|
22
|
+
response: DMARCResult | false;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Signing domains (normalized) of the DKIM signatures that aligned
|
|
26
|
+
*/
|
|
27
|
+
dkimAligned?: Set<string>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Why DMARC validation was not possible (RFC 9989 5.3.1), set when response is false
|
|
31
|
+
*/
|
|
32
|
+
reason?: 'no-author-domain' | 'multiple-author-domains' | 'invalid-author-domain' | 'multiple-from-fields';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The Author Domains found, set when response is false
|
|
36
|
+
*/
|
|
37
|
+
authorDomains?: string[];
|
|
38
|
+
}>;
|