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.
Files changed (58) hide show
  1. package/.c8rc.json +11 -0
  2. package/.prettierignore +3 -0
  3. package/.release-please-manifest.json +1 -1
  4. package/CHANGELOG.md +68 -0
  5. package/README.md +131 -31
  6. package/bin/mailauth.js +62 -0
  7. package/cli.md +150 -8
  8. package/docs/arc.md +62 -54
  9. package/docs/bimi.md +153 -54
  10. package/docs/dkim.md +37 -36
  11. package/docs/dkim2.md +154 -0
  12. package/docs/dmarc.md +4 -1
  13. package/docs/mta-sts.md +12 -9
  14. package/docs/spf.md +14 -13
  15. package/index.d.ts +583 -42
  16. package/lib/arc/index.d.ts +6 -2
  17. package/lib/arc/index.js +45 -15
  18. package/lib/bimi/index.js +362 -102
  19. package/lib/bimi/validate-svg.js +493 -60
  20. package/lib/commands/dkim2-hash.js +25 -0
  21. package/lib/commands/dkim2-sign.js +77 -0
  22. package/lib/commands/dkim2-verify.js +59 -0
  23. package/lib/commands/report.js +10 -35
  24. package/lib/commands/resolver.js +51 -0
  25. package/lib/commands/seal.js +10 -38
  26. package/lib/commands/spf.js +4 -35
  27. package/lib/dkim/dkim-signer.js +50 -30
  28. package/lib/dkim/dkim-verifier.js +79 -27
  29. package/lib/dkim/header/simple.js +2 -2
  30. package/lib/dkim/sign.js +7 -2
  31. package/lib/dkim/syntax.js +11 -0
  32. package/lib/dkim/verify.js +1 -0
  33. package/lib/dkim2/dkim2-parser.js +52 -0
  34. package/lib/dkim2/dkim2-signer.js +334 -0
  35. package/lib/dkim2/dkim2-verifier.js +572 -0
  36. package/lib/dkim2/fields.js +570 -0
  37. package/lib/dkim2/hash.d.ts +3 -0
  38. package/lib/dkim2/hash.js +51 -0
  39. package/lib/dkim2/key.js +203 -0
  40. package/lib/dkim2/message-state.js +181 -0
  41. package/lib/dkim2/recipe.js +134 -0
  42. package/lib/dkim2/sign.d.ts +3 -0
  43. package/lib/dkim2/sign.js +38 -0
  44. package/lib/dkim2/verify.d.ts +3 -0
  45. package/lib/dkim2/verify.js +19 -0
  46. package/lib/dmarc/author-domain.js +270 -5
  47. package/lib/dmarc/get-dmarc-record.js +24 -11
  48. package/lib/dmarc/verify.js +7 -3
  49. package/lib/mailauth.js +57 -14
  50. package/lib/mta-sts.js +37 -11
  51. package/lib/parse-received.js +214 -25
  52. package/lib/spf/index.js +21 -4
  53. package/lib/spf/macro.js +5 -3
  54. package/lib/spf/spf-verify.js +14 -5
  55. package/lib/spf/syntax.js +5 -0
  56. package/lib/tools.js +160 -21
  57. package/package.json +15 -6
  58. 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 = typeof fieldNames === 'string' ? normalizeFieldNames(fieldNames) : defaultDKIMFieldNamesNormalized;
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.map(entry => entry.casedKey).join(': '),
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 list = await resolver(toALabel(name), 'TXT');
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
- .replace(/\s+/g, '');
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
- if (version.toLowerCase().trim() !== 'dkim1') {
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(tags.s.value);
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 ? (tags.k.value || '').toString().toLowerCase().trim() : false;
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 (strict && keyType === 'ed25519' && rawPublicKey.length !== 32) {
563
- // RFC 8463 section 4 publishes the bare key, not a DER structure
564
- throw keyError('Invalid ed25519 public key', 'EINVALIDVAL', rr);
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(tags.t?.value);
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 (DOMAIN_NAME.test(domain)) {
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 anything
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 = formatDomain(tldts.getDomain(fromDomain, TLDTS_OPTS) || fromDomain);
1089
+ let fromOrg = getOrgDomain(fromDomain);
954
1090
  if (!fromOrg) {
955
1091
  return false;
956
1092
  }
957
1093
  for (let entry of domainList) {
958
- let entryOrg = formatDomain(tldts.getDomain(entry.domain, TLDTS_OPTS) || entry.domain);
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.1.1",
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
- "js-yaml": "5.4.2",
42
- "license-report": "6.8.5",
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.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.14",
61
+ "nodemailer": "10.1.0",
56
62
  "punycode.js": "2.3.1",
57
- "tldts": "7.4.16",
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
+ }