mailauth 7.1.1 → 7.3.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/.c8rc.json +11 -0
- package/.prettierignore +3 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +68 -0
- package/README.md +131 -31
- package/bin/mailauth.js +62 -0
- package/cli.md +150 -8
- package/docs/arc.md +62 -54
- package/docs/bimi.md +153 -54
- package/docs/dkim.md +37 -36
- package/docs/dkim2.md +154 -0
- package/docs/dmarc.md +4 -1
- package/docs/mta-sts.md +12 -9
- package/docs/spf.md +14 -13
- package/index.d.ts +583 -42
- package/lib/arc/index.d.ts +6 -2
- package/lib/arc/index.js +45 -15
- package/lib/bimi/index.js +362 -102
- package/lib/bimi/validate-svg.js +493 -60
- package/lib/commands/dkim2-hash.js +25 -0
- package/lib/commands/dkim2-sign.js +77 -0
- package/lib/commands/dkim2-verify.js +59 -0
- package/lib/commands/report.js +10 -35
- package/lib/commands/resolver.js +51 -0
- package/lib/commands/seal.js +10 -38
- package/lib/commands/spf.js +4 -35
- package/lib/dkim/dkim-signer.js +50 -30
- package/lib/dkim/dkim-verifier.js +79 -27
- package/lib/dkim/header/simple.js +2 -2
- package/lib/dkim/sign.js +7 -2
- package/lib/dkim/syntax.js +11 -0
- package/lib/dkim/verify.js +1 -0
- package/lib/dkim2/dkim2-parser.js +52 -0
- package/lib/dkim2/dkim2-signer.js +334 -0
- package/lib/dkim2/dkim2-verifier.js +572 -0
- package/lib/dkim2/fields.js +570 -0
- package/lib/dkim2/hash.d.ts +3 -0
- package/lib/dkim2/hash.js +51 -0
- package/lib/dkim2/key.js +203 -0
- package/lib/dkim2/message-state.js +181 -0
- package/lib/dkim2/recipe.js +134 -0
- package/lib/dkim2/sign.d.ts +3 -0
- package/lib/dkim2/sign.js +38 -0
- package/lib/dkim2/verify.d.ts +3 -0
- package/lib/dkim2/verify.js +19 -0
- package/lib/dmarc/author-domain.js +270 -5
- package/lib/dmarc/get-dmarc-record.js +24 -11
- package/lib/dmarc/verify.js +7 -3
- package/lib/mailauth.js +57 -14
- package/lib/mta-sts.js +37 -11
- package/lib/parse-received.js +214 -25
- package/lib/spf/index.js +21 -4
- package/lib/spf/macro.js +5 -3
- package/lib/spf/spf-verify.js +14 -5
- package/lib/spf/syntax.js +5 -0
- package/lib/tools.js +160 -21
- package/package.json +15 -6
- package/stryker.config.json +16 -0
package/lib/tools.js
CHANGED
|
@@ -13,7 +13,7 @@ const parseDkimHeaders = require('./parse-dkim-headers');
|
|
|
13
13
|
const { parseTagList } = parseDkimHeaders;
|
|
14
14
|
const tldts = require('tldts');
|
|
15
15
|
const Joi = require('joi');
|
|
16
|
-
const { DOMAIN_NAME } = require('./dkim/syntax');
|
|
16
|
+
const { DOMAIN_NAME, HOST_NAME_CHARS } = require('./dkim/syntax');
|
|
17
17
|
const base64Schema = Joi.string().base64({ paddingRequired: false });
|
|
18
18
|
|
|
19
19
|
const defaultDKIMFieldNames =
|
|
@@ -33,7 +33,10 @@ const keyOrderingAS = ['i', 'a', 't', 'cv', 'd', 's', 'b'];
|
|
|
33
33
|
|
|
34
34
|
const TLDTS_OPTS = {
|
|
35
35
|
allowIcannDomains: true,
|
|
36
|
-
allowPrivateDomains: true
|
|
36
|
+
allowPrivateDomains: true,
|
|
37
|
+
// the input is a host name, not a URL. Otherwise "example.com/x.example.net" would be read
|
|
38
|
+
// as the URL of the host "example.com"
|
|
39
|
+
extractHostname: false
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
/**
|
|
@@ -68,6 +71,10 @@ const toALabel = domain => {
|
|
|
68
71
|
.join('.');
|
|
69
72
|
};
|
|
70
73
|
|
|
74
|
+
// Whether a d= value only has the characters of a host name once U-labels are converted to
|
|
75
|
+
// A-labels (see HOST_NAME_CHARS in lib/dkim/syntax.js)
|
|
76
|
+
const isHostNameDomain = domain => HOST_NAME_CHARS.test(toALabel(String(domain)));
|
|
77
|
+
|
|
71
78
|
// lower case A-label form of a domain without the root dot, for comparing domains
|
|
72
79
|
const normalizeDomainName = domain =>
|
|
73
80
|
toALabel((domain || '').toString().trim())
|
|
@@ -201,12 +208,51 @@ const normalizeFieldNames = fieldNames =>
|
|
|
201
208
|
|
|
202
209
|
const defaultDKIMFieldNamesNormalized = normalizeFieldNames(defaultDKIMFieldNames);
|
|
203
210
|
|
|
211
|
+
// A field name that can be written into h= although the message has no such field: RFC 5322
|
|
212
|
+
// ftext without the ";" that would end the tag (RFC 6376 section 3.2)
|
|
213
|
+
const OVERSIGN_FIELD_NAME = /^[\x21-\x39\x3c-\x7e]+$/;
|
|
214
|
+
|
|
215
|
+
// The names a signer lists in h= more times than the message has fields of that name
|
|
216
|
+
// (RFC 6376 sections 5.4.2 and 8.15), so that adding such a field breaks the signature. Only a
|
|
217
|
+
// name the header list repeats is over-signed: it appears in h= as many times as it is listed,
|
|
218
|
+
// or as many times as the message has it, whichever is more
|
|
219
|
+
const getOversignedNames = (fieldNames, signingList) => {
|
|
220
|
+
let listed = new Map();
|
|
221
|
+
for (let casedKey of fieldNames.split(':')) {
|
|
222
|
+
casedKey = casedKey.replace(/^[ \t]+|[ \t]+$/g, '');
|
|
223
|
+
let key = lowerCaseASCII(casedKey);
|
|
224
|
+
if (!key) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (!listed.has(key)) {
|
|
228
|
+
listed.set(key, { casedKey, count: 0 });
|
|
229
|
+
}
|
|
230
|
+
listed.get(key).count++;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let extra = [];
|
|
234
|
+
for (let [key, { casedKey, count }] of listed) {
|
|
235
|
+
if (count < 2 || !OVERSIGN_FIELD_NAME.test(casedKey)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
let present = signingList.filter(header => header.key === key).length;
|
|
239
|
+
for (let i = present; i < count; i++) {
|
|
240
|
+
extra.push(casedKey);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return extra;
|
|
244
|
+
};
|
|
245
|
+
|
|
204
246
|
const getSigningHeaderLines = (parsedHeaders, fieldNames, verify) => {
|
|
247
|
+
let configuredFieldNames = typeof fieldNames === 'string' ? fieldNames : false;
|
|
248
|
+
|
|
205
249
|
// an h= tag is attacker controlled on inbound mail, so only the constant default list
|
|
206
250
|
// is normalized ahead of time
|
|
207
|
-
fieldNames =
|
|
251
|
+
fieldNames = configuredFieldNames !== false ? normalizeFieldNames(configuredFieldNames) : defaultDKIMFieldNamesNormalized;
|
|
208
252
|
|
|
209
253
|
let signingList = [];
|
|
254
|
+
// names listed in h= without a header field to hash for them
|
|
255
|
+
let oversigned = [];
|
|
210
256
|
|
|
211
257
|
if (verify) {
|
|
212
258
|
let parsedList = [].concat(parsedHeaders);
|
|
@@ -227,14 +273,36 @@ const getSigningHeaderLines = (parsedHeaders, fieldNames, verify) => {
|
|
|
227
273
|
signingList.push(header);
|
|
228
274
|
}
|
|
229
275
|
}
|
|
276
|
+
|
|
277
|
+
if (configuredFieldNames) {
|
|
278
|
+
oversigned = getOversignedNames(configuredFieldNames, signingList);
|
|
279
|
+
}
|
|
230
280
|
}
|
|
231
281
|
|
|
232
282
|
return {
|
|
233
|
-
keys: signingList
|
|
283
|
+
keys: signingList
|
|
284
|
+
.map(entry => entry.casedKey)
|
|
285
|
+
.concat(oversigned)
|
|
286
|
+
.join(': '),
|
|
234
287
|
headers: signingList
|
|
235
288
|
};
|
|
236
289
|
};
|
|
237
290
|
|
|
291
|
+
// RFC 6376 section 2.11 dkim-quoted-printable, for the i= tag of a DKIM-Signature. Every byte
|
|
292
|
+
// of the UTF-8 encoding that is not a dkim-safe-char (%x21-3A / %x3C / %x3E-7E) is written as
|
|
293
|
+
// "=XX". This includes "=" itself, which a verifier would otherwise read as an escape
|
|
294
|
+
const encodeDkimQuotedPrintable = value => {
|
|
295
|
+
let result = '';
|
|
296
|
+
for (let byte of Buffer.from((value || '').toString(), 'utf8')) {
|
|
297
|
+
if (byte >= 0x21 && byte <= 0x7e && byte !== 0x3b && byte !== 0x3d) {
|
|
298
|
+
result += String.fromCharCode(byte);
|
|
299
|
+
} else {
|
|
300
|
+
result += '=' + byte.toString(16).toUpperCase().padStart(2, '0');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
};
|
|
305
|
+
|
|
238
306
|
/**
|
|
239
307
|
* Generates `DKIM-Signature: ...` header for selected values
|
|
240
308
|
* @param {Object} values
|
|
@@ -312,6 +380,9 @@ const formatSignatureHeaderLine = (type, values, folded) => {
|
|
|
312
380
|
// convert to A-label if needed
|
|
313
381
|
val = val.substr(0, atPos + 1) + toALabel(val.substr(atPos + 1));
|
|
314
382
|
}
|
|
383
|
+
// the AUID is dkim-quoted-printable, a raw non-ASCII or "=" in it would be
|
|
384
|
+
// read back as something else
|
|
385
|
+
val = encodeDkimQuotedPrintable(val);
|
|
315
386
|
}
|
|
316
387
|
|
|
317
388
|
return `${key}=${val}`;
|
|
@@ -360,6 +431,8 @@ const addWarning = (list, ...items) => {
|
|
|
360
431
|
const KEY_ERROR_COMMENTS = new Map([
|
|
361
432
|
['ENOTFOUND', 'no key'],
|
|
362
433
|
['ENODATA', 'no key'],
|
|
434
|
+
// a key record name that can not exist in the DNS, so there is no key, now or later
|
|
435
|
+
['EBADNAME', 'no key'],
|
|
363
436
|
['EINVALIDVER', 'unknown key version'],
|
|
364
437
|
['EINVALIDTYPE', 'unknown key type'],
|
|
365
438
|
['EINVALIDVAL', 'invalid public key'],
|
|
@@ -414,6 +487,18 @@ const isBareRsaPublicKey = der => {
|
|
|
414
487
|
return der[headerLength] === 0x02;
|
|
415
488
|
};
|
|
416
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Whether a name can exist in the DNS (RFC 1035 section 2.3.4): no empty label, labels of at
|
|
492
|
+
* most 63 octets and a name of at most 253 octets, not counting a trailing root dot
|
|
493
|
+
*
|
|
494
|
+
* @param {String} name Domain name, with A-labels
|
|
495
|
+
* @returns {Boolean} true when the name can be looked up
|
|
496
|
+
*/
|
|
497
|
+
const isValidDnsName = name => {
|
|
498
|
+
name = (name || '').toString().replace(/\.$/, '');
|
|
499
|
+
return Buffer.byteLength(name) <= 253 && name.split('.').every(label => label.length && Buffer.byteLength(label) <= 63);
|
|
500
|
+
};
|
|
501
|
+
|
|
417
502
|
/**
|
|
418
503
|
* Fetches and validates a DKIM public key record (RFC 6376 section 3.6.1, RFC 8463)
|
|
419
504
|
*
|
|
@@ -434,13 +519,21 @@ const getPublicKey = async (type, name, minBitLength, resolver, options) => {
|
|
|
434
519
|
const strict = !!options.strict;
|
|
435
520
|
|
|
436
521
|
// RFC 8616 section 3: a U-label is converted to an A-label before it is looked up
|
|
437
|
-
let
|
|
522
|
+
let lookupName = toALabel(name);
|
|
523
|
+
if (!isValidDnsName(lookupName)) {
|
|
524
|
+
// the resolver would fail with an error that looks transient, or ask for a name that
|
|
525
|
+
// can not have a record at all
|
|
526
|
+
throw keyError('Invalid key record name', 'EBADNAME');
|
|
527
|
+
}
|
|
528
|
+
let list = await resolver(lookupName, 'TXT');
|
|
529
|
+
// the record is parsed as it is, whitespace is only removed from p= (by the tag-list
|
|
530
|
+
// parser), where base64string allows FWS anywhere (RFC 6376 section 3.6.1)
|
|
438
531
|
let rr =
|
|
439
532
|
list &&
|
|
440
533
|
[]
|
|
441
534
|
.concat(list[0] || [])
|
|
442
535
|
.join('')
|
|
443
|
-
.
|
|
536
|
+
.trim();
|
|
444
537
|
|
|
445
538
|
if (!rr) {
|
|
446
539
|
throw keyError('Missing key value', 'EINVALIDVAL');
|
|
@@ -459,6 +552,18 @@ const getPublicKey = async (type, name, minBitLength, resolver, options) => {
|
|
|
459
552
|
let entry = parseTagList(rr, { strict });
|
|
460
553
|
const tags = entry.parsed;
|
|
461
554
|
|
|
555
|
+
// The value of a k=, s= or t= tag. FWS is allowed around "=", ";" and the ":" of a list,
|
|
556
|
+
// not inside a value (RFC 6376 section 3.6.1), so strict mode reads the value as it is. The
|
|
557
|
+
// lenient mode removes the whitespace, as it always has
|
|
558
|
+
const tagValue = name => {
|
|
559
|
+
let value = typeof tags[name]?.value === 'string' ? tags[name].value : '';
|
|
560
|
+
if (/\s/.test(splitKeyList(value).join(':'))) {
|
|
561
|
+
violation(`Invalid key record syntax: whitespace in ${name}=`, 'EINVALIDVAL', 'key-syntax');
|
|
562
|
+
return value.replace(/\s+/g, '');
|
|
563
|
+
}
|
|
564
|
+
return value;
|
|
565
|
+
};
|
|
566
|
+
|
|
462
567
|
if (entry.syntaxErrors.length) {
|
|
463
568
|
// RFC 6376 section 6.1.2 step 5: a key record that does not adhere to the format is ignored
|
|
464
569
|
violation(`Invalid key record syntax: ${entry.syntaxErrors[0]}`, 'EINVALIDVAL', 'key-syntax');
|
|
@@ -479,7 +584,8 @@ const getPublicKey = async (type, name, minBitLength, resolver, options) => {
|
|
|
479
584
|
// rules apply to them as well
|
|
480
585
|
if (tags.v) {
|
|
481
586
|
let version = (tags.v.value || '').toString();
|
|
482
|
-
|
|
587
|
+
// the lenient mode reads "DKIM 1" as DKIM1, the exact check below marks it
|
|
588
|
+
if (version.replace(/\s+/g, '').toLowerCase() !== 'dkim1') {
|
|
483
589
|
throw keyError('Unknown key version', 'EINVALIDVER', rr);
|
|
484
590
|
}
|
|
485
591
|
|
|
@@ -492,7 +598,7 @@ const getPublicKey = async (type, name, minBitLength, resolver, options) => {
|
|
|
492
598
|
// RFC 6376 section 3.6.1 s=: "Verifiers for a given service type MUST ignore this record
|
|
493
599
|
// if the appropriate type is not listed", applied in every mode, it is the key owner's choice
|
|
494
600
|
if (tags.s) {
|
|
495
|
-
let services = splitKeyList(
|
|
601
|
+
let services = splitKeyList(tagValue('s'));
|
|
496
602
|
if (!services.includes('email') && !services.includes('*')) {
|
|
497
603
|
throw keyError('Key is not meant for email', 'EINVALIDSERVICE', rr);
|
|
498
604
|
}
|
|
@@ -507,7 +613,7 @@ const getPublicKey = async (type, name, minBitLength, resolver, options) => {
|
|
|
507
613
|
}
|
|
508
614
|
}
|
|
509
615
|
|
|
510
|
-
let declaredKeyType = tags.k ? (
|
|
616
|
+
let declaredKeyType = tags.k ? tagValue('k').toLowerCase().trim() : false;
|
|
511
617
|
|
|
512
618
|
let rawPublicKey = Buffer.from(publicKeyValue, 'base64');
|
|
513
619
|
let publicKeyObj;
|
|
@@ -559,9 +665,9 @@ const getPublicKey = async (type, name, minBitLength, resolver, options) => {
|
|
|
559
665
|
violation(`Unknown key type (${keyType})`, 'EINVALIDTYPE', 'key-type-inferred');
|
|
560
666
|
}
|
|
561
667
|
|
|
562
|
-
if (
|
|
563
|
-
// RFC 8463 section 4 publishes the bare key, not a
|
|
564
|
-
|
|
668
|
+
if (keyType === 'ed25519' && rawPublicKey.length !== 32) {
|
|
669
|
+
// RFC 8463 section 4 publishes the bare key, not a SubjectPublicKeyInfo structure
|
|
670
|
+
violation('Invalid ed25519 public key', 'EINVALIDVAL', 'key-ed25519-spki');
|
|
565
671
|
}
|
|
566
672
|
|
|
567
673
|
let modulusLength = publicKeyObj.asymmetricKeyDetails.modulusLength;
|
|
@@ -571,7 +677,7 @@ const getPublicKey = async (type, name, minBitLength, resolver, options) => {
|
|
|
571
677
|
}
|
|
572
678
|
|
|
573
679
|
// t= flags. "y" is the testing mode, "s" forbids an i= domain that is a subdomain of d=
|
|
574
|
-
let flags = splitKeyList(
|
|
680
|
+
let flags = splitKeyList(tagValue('t'));
|
|
575
681
|
|
|
576
682
|
return {
|
|
577
683
|
publicKey: exportedKey,
|
|
@@ -727,12 +833,21 @@ const escapePropValue = (value, strict) => {
|
|
|
727
833
|
return value;
|
|
728
834
|
}
|
|
729
835
|
|
|
836
|
+
// RFC 8601 section 1.5.2 extends domain-name to U-labels, a domain with U-labels is
|
|
837
|
+
// checked in its A-label form. The default mode quotes it, as older versions did
|
|
838
|
+
let isDomainName = domain => DOMAIN_NAME.test(domain) || (strict && NON_ASCII.test(domain) && DOMAIN_NAME.test(toALabel(domain)));
|
|
839
|
+
|
|
840
|
+
if (strict && isDomainName(value)) {
|
|
841
|
+
// the domain-name branch of pvalue, without a local-part and "@"
|
|
842
|
+
return value;
|
|
843
|
+
}
|
|
844
|
+
|
|
730
845
|
let atPos = value.lastIndexOf('@');
|
|
731
846
|
if (atPos >= 0) {
|
|
732
847
|
let localPart = value.substring(0, atPos);
|
|
733
848
|
let domain = value.substring(atPos + 1);
|
|
734
849
|
|
|
735
|
-
if (
|
|
850
|
+
if (isDomainName(domain)) {
|
|
736
851
|
if (!localPart || DOT_ATOM_TEXT.test(localPart)) {
|
|
737
852
|
// a valid [ [ local-part ] "@" ] domain-name as it is
|
|
738
853
|
if (strict || LEGACY_UNQUOTED.test(value)) {
|
|
@@ -857,12 +972,17 @@ const formatAuthHeaderRow = (method, status, options) => {
|
|
|
857
972
|
// RFC 6376 section 3.4.2. Only SP and HTAB are whitespace here: the line is a
|
|
858
973
|
// 'binary' string, so a byte such as 0xA0 (the second byte of a UTF-8
|
|
859
974
|
// non-breaking space) must not be collapsed or trimmed
|
|
975
|
+
// The bytes of a header line for canonicalization. A string is a header line built here, and it
|
|
976
|
+
// is written out as UTF-8, so it is hashed as the same UTF-8 bytes. A Buffer holds the bytes of
|
|
977
|
+
// the message as they are
|
|
978
|
+
const toLineBytes = line => (typeof line === 'string' ? Buffer.from(line) : line);
|
|
979
|
+
|
|
860
980
|
const formatRelaxedLine = (line, suffix) => {
|
|
861
981
|
let result =
|
|
862
982
|
// a missing line is nothing to canonicalize. Optional chaining would resolve the
|
|
863
983
|
// whole chain to undefined and the concatenation below would then hash the
|
|
864
984
|
// 9 byte string "undefined" as if it were a header
|
|
865
|
-
(line ||
|
|
985
|
+
(toLineBytes(line) || Buffer.alloc(0))
|
|
866
986
|
.toString('binary')
|
|
867
987
|
// unfold
|
|
868
988
|
.replace(/\r?\n/g, '')
|
|
@@ -917,6 +1037,21 @@ const formatDomain = domain => {
|
|
|
917
1037
|
return domain.replace(/\.+$/, '');
|
|
918
1038
|
};
|
|
919
1039
|
|
|
1040
|
+
// A domain as getAlignment() compares it, after formatDomain(): labels of letters, digits, "-"
|
|
1041
|
+
// and "_" (which the lenient mode lets through), with no empty label. Anything else, such as
|
|
1042
|
+
// the URL delimiters "/", "?" and "#", can not be a host name and never aligns
|
|
1043
|
+
const ALIGNMENT_DOMAIN = /^[a-z0-9_-]+(?:\.[a-z0-9_-]+)*$/;
|
|
1044
|
+
|
|
1045
|
+
// the Organizational Domain of a domain as found from the Public Suffix List, or false for a
|
|
1046
|
+
// value that is not a host name
|
|
1047
|
+
const getOrgDomain = domain => {
|
|
1048
|
+
domain = typeof domain === 'string' ? formatDomain(domain) : '';
|
|
1049
|
+
if (!ALIGNMENT_DOMAIN.test(domain)) {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
return tldts.getDomain(domain, TLDTS_OPTS) || domain;
|
|
1053
|
+
};
|
|
1054
|
+
|
|
920
1055
|
const getAlignment = (fromDomain, domainList, strict) => {
|
|
921
1056
|
// This argument used to be an options object. Every object is truthy, so a leftover
|
|
922
1057
|
// { strict: false } call would otherwise mean hard strict, the opposite of what it asks for.
|
|
@@ -936,9 +1071,10 @@ const getAlignment = (fromDomain, domainList, strict) => {
|
|
|
936
1071
|
|
|
937
1072
|
if (strict) {
|
|
938
1073
|
// strict alignment: the domains must be identical (RFC 9989 §3.2.10.2)
|
|
939
|
-
let from = formatDomain(fromDomain);
|
|
940
|
-
if (!from) {
|
|
941
|
-
// a bare root label normalizes to an empty string, which must not align with
|
|
1074
|
+
let from = typeof fromDomain === 'string' ? formatDomain(fromDomain) : '';
|
|
1075
|
+
if (!ALIGNMENT_DOMAIN.test(from)) {
|
|
1076
|
+
// a bare root label normalizes to an empty string, which must not align with
|
|
1077
|
+
// anything, and neither does a value that is not a host name
|
|
942
1078
|
return false;
|
|
943
1079
|
}
|
|
944
1080
|
for (let entry of domainList) {
|
|
@@ -950,13 +1086,12 @@ const getAlignment = (fromDomain, domainList, strict) => {
|
|
|
950
1086
|
}
|
|
951
1087
|
|
|
952
1088
|
// relaxed alignment: the domains must share an Organizational Domain (RFC 9989 §3.2.10.1)
|
|
953
|
-
let fromOrg =
|
|
1089
|
+
let fromOrg = getOrgDomain(fromDomain);
|
|
954
1090
|
if (!fromOrg) {
|
|
955
1091
|
return false;
|
|
956
1092
|
}
|
|
957
1093
|
for (let entry of domainList) {
|
|
958
|
-
|
|
959
|
-
if (entryOrg === fromOrg) {
|
|
1094
|
+
if (getOrgDomain(entry.domain) === fromOrg) {
|
|
960
1095
|
return entry;
|
|
961
1096
|
}
|
|
962
1097
|
}
|
|
@@ -1221,6 +1356,8 @@ module.exports = {
|
|
|
1221
1356
|
createError,
|
|
1222
1357
|
addWarning,
|
|
1223
1358
|
KEY_ERROR_COMMENTS,
|
|
1359
|
+
isValidDnsName,
|
|
1360
|
+
isBareRsaPublicKey,
|
|
1224
1361
|
|
|
1225
1362
|
defaultDKIMFieldNames,
|
|
1226
1363
|
defaultARCFieldNames,
|
|
@@ -1247,10 +1384,12 @@ module.exports = {
|
|
|
1247
1384
|
|
|
1248
1385
|
getAlignment,
|
|
1249
1386
|
|
|
1387
|
+
toLineBytes,
|
|
1250
1388
|
formatRelaxedLine,
|
|
1251
1389
|
stripSignatureValue,
|
|
1252
1390
|
formatDomain,
|
|
1253
1391
|
toALabel,
|
|
1392
|
+
isHostNameDomain,
|
|
1254
1393
|
normalizeDomainName,
|
|
1255
1394
|
isSameOrSubdomain,
|
|
1256
1395
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mailauth",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.3.0",
|
|
4
4
|
"description": "Email authentication library for Node.js",
|
|
5
5
|
"main": "lib/mailauth.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "eslint \"lib/**/*.js\" \"test/**/*.js\" && mocha --recursive \"./test/**/*-test.js\" --reporter spec",
|
|
9
|
+
"coverage": "c8 mocha --recursive \"./test/**/*-test.js\" --reporter dot",
|
|
10
|
+
"coverage:check": "c8 check-coverage",
|
|
11
|
+
"mutation": "stryker run",
|
|
9
12
|
"format": "prettier --write .",
|
|
10
13
|
"build-source": "rm -rf node_modules package-lock.json && npm install && npm run licenses && rm -rf node_modules package-lock.json && npm install --production && rm -rf package-lock.json",
|
|
11
14
|
"build-dist": "pkg --compress Brotli package.json && rm -rf package-lock.json && npm install && node winconf.js",
|
|
@@ -34,15 +37,18 @@
|
|
|
34
37
|
},
|
|
35
38
|
"homepage": "https://github.com/postalsys/mailauth",
|
|
36
39
|
"devDependencies": {
|
|
40
|
+
"@stryker-mutator/core": "10.0.0",
|
|
41
|
+
"c8": "12.0.0",
|
|
37
42
|
"chai": "4.4.1",
|
|
38
43
|
"eslint": "10.12.0",
|
|
39
44
|
"eslint-config-nodemailer": "1.2.0",
|
|
40
45
|
"eslint-config-prettier": "10.1.8",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
46
|
+
"fast-check": "4.10.2",
|
|
47
|
+
"js-yaml": "5.4.3",
|
|
48
|
+
"license-report": "6.8.6",
|
|
43
49
|
"mbox-reader": "1.2.0",
|
|
44
50
|
"mocha": "12.0.3",
|
|
45
|
-
"prettier": "3.9.
|
|
51
|
+
"prettier": "3.9.10",
|
|
46
52
|
"resedit": "3.1.0"
|
|
47
53
|
},
|
|
48
54
|
"dependencies": {
|
|
@@ -52,9 +58,9 @@
|
|
|
52
58
|
"ipaddr.js": "2.5.0",
|
|
53
59
|
"joi": "18.2.9",
|
|
54
60
|
"libmime": "5.4.7",
|
|
55
|
-
"nodemailer": "10.0
|
|
61
|
+
"nodemailer": "10.1.0",
|
|
56
62
|
"punycode.js": "2.3.1",
|
|
57
|
-
"tldts": "7.4.
|
|
63
|
+
"tldts": "7.4.18",
|
|
58
64
|
"undici": "8.11.2"
|
|
59
65
|
},
|
|
60
66
|
"engines": {
|
|
@@ -75,5 +81,8 @@
|
|
|
75
81
|
"node24-win-x64"
|
|
76
82
|
],
|
|
77
83
|
"outputPath": "ee-dist"
|
|
84
|
+
},
|
|
85
|
+
"overrides": {
|
|
86
|
+
"qs": "6.16.0"
|
|
78
87
|
}
|
|
79
88
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
|
|
3
|
+
"_comment": "Opt-in mutation testing, run with `npm run mutation`. The mocha runner plugin does not support mocha 12, so every mutant runs the tests of the mutated modules through the command runner. The long mocha timeout keeps the slower instrumented code from failing tests by timeout, which would count as a killed mutant",
|
|
4
|
+
"testRunner": "command",
|
|
5
|
+
"commandRunner": {
|
|
6
|
+
"command": "npx mocha --bail --timeout 60000 --reporter dot \"test/dkim/**/*-test.js\" \"test/spf/**/*-test.js\" \"test/dmarc/**/*-test.js\" \"test/dkim2/**/*-test.js\" \"test/property/*-test.js\" test/mailauth-test.js"
|
|
7
|
+
},
|
|
8
|
+
"coverageAnalysis": "off",
|
|
9
|
+
"mutate": ["lib/dkim/body/*.js", "lib/spf/macro.js", "lib/dmarc/author-domain.js", "lib/dkim2/*.js"],
|
|
10
|
+
"reporters": ["clear-text", "progress", "html", "json"],
|
|
11
|
+
"htmlReporter": { "fileName": "reports/mutation/mutation.html" },
|
|
12
|
+
"jsonReporter": { "fileName": "reports/mutation/mutation.json" },
|
|
13
|
+
"tempDirName": ".stryker-tmp",
|
|
14
|
+
"cleanTempDir": true,
|
|
15
|
+
"timeoutMS": 60000
|
|
16
|
+
}
|