mailauth 6.0.0 → 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.
Files changed (47) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +111 -39
  4. package/bin/mailauth.js +14 -6
  5. package/cli.md +8 -3
  6. package/docs/arc.md +126 -43
  7. package/docs/bimi.md +54 -37
  8. package/docs/dkim.md +79 -40
  9. package/docs/dmarc.md +80 -33
  10. package/docs/mta-sts.md +140 -42
  11. package/docs/spf.md +69 -4
  12. package/index.d.ts +495 -43
  13. package/lib/arc/index.d.ts +10 -8
  14. package/lib/arc/index.js +505 -124
  15. package/lib/bimi/index.js +162 -81
  16. package/lib/commands/report.js +2 -1
  17. package/lib/commands/seal.js +35 -5
  18. package/lib/commands/sign.js +8 -1
  19. package/lib/commands/spf.js +8 -0
  20. package/lib/dkim/body/relaxed.js +6 -5
  21. package/lib/dkim/body/simple.js +1 -3
  22. package/lib/dkim/dkim-signer.js +219 -33
  23. package/lib/dkim/dkim-verifier.js +574 -231
  24. package/lib/dkim/header/relaxed.js +6 -43
  25. package/lib/dkim/header/signature-opts.js +87 -0
  26. package/lib/dkim/header/simple.js +6 -43
  27. package/lib/dkim/message-parser.js +18 -4
  28. package/lib/dkim/sign.d.ts +13 -7
  29. package/lib/dkim/sign.js +18 -2
  30. package/lib/dkim/syntax.js +31 -0
  31. package/lib/dkim/verify.js +1 -0
  32. package/lib/dmarc/author-domain.js +181 -0
  33. package/lib/dmarc/get-dmarc-record.js +140 -47
  34. package/lib/dmarc/index.d.ts +29 -2
  35. package/lib/dmarc/verify.js +120 -44
  36. package/lib/mailauth.js +75 -36
  37. package/lib/mta-sts.d.ts +77 -11
  38. package/lib/mta-sts.js +497 -182
  39. package/lib/parse-dkim-headers.js +376 -191
  40. package/lib/parse-received.js +104 -32
  41. package/lib/spf/index.d.ts +37 -1
  42. package/lib/spf/index.js +260 -28
  43. package/lib/spf/macro.js +50 -13
  44. package/lib/spf/spf-verify.js +440 -204
  45. package/lib/spf/syntax.js +366 -0
  46. package/lib/tools.js +522 -116
  47. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -84,6 +84,24 @@ export interface AuthenticateOptions {
84
84
  * ARC sealing options
85
85
  */
86
86
  seal?: ARCSealOptions;
87
+
88
+ /**
89
+ * Follow the RFCs exactly instead of the lenient defaults (default: false). Passed on
90
+ * to every check (DKIM, SPF, ARC, DMARC, BIMI and ARC sealing), BIMI only uses it for the
91
+ * format of its Authentication-Results entry. In strict mode:
92
+ *
93
+ * - rsa-sha1 DKIM signatures are reported as `dkim=policy` (`policy.dkim-rules=weak-algorithm`)
94
+ * - DKIM-Signature and key record syntax is validated (RFC 6376 sections 3.2, 3.5, 3.6.1, 6.1.1)
95
+ * - a body hash mismatch is reported as `dkim=fail`, and a signature that can not be
96
+ * processed as `dkim=neutral` instead of being left out
97
+ * - DKIM results report `header.d` and the AUID as `header.i`
98
+ * - email identities in Authentication-Results use the `local-part@domain` form of RFC 8601 section 2.2
99
+ * - Authentication-Results is placed above Received-SPF (RFC 8601 section 5)
100
+ *
101
+ * What the lenient default accepts and strict mode would reject is listed in the
102
+ * `warnings` array of the affected result. It is never written into the headers
103
+ */
104
+ strict?: boolean;
87
105
  }
88
106
 
89
107
  /**
@@ -111,20 +129,47 @@ export interface ARCSealOptions {
111
129
  canonicalization?: string;
112
130
 
113
131
  /**
114
- * Signing algorithm (default: 'rsa-sha256')
115
- * Supported: 'rsa-sha256', 'ed25519-sha256'
132
+ * Signing algorithm, 'rsa-sha256' or 'ed25519-sha256'. Follows the private key type when
133
+ * not set. Used for both the ARC-Message-Signature and the ARC-Seal
116
134
  */
117
135
  algorithm?: string;
118
136
 
119
137
  /**
120
- * Headers to include in signature
138
+ * Chain validation status for the ARC-Seal cv= tag: 'none', 'pass' or 'fail' (compared
139
+ * case-insensitively). Defaults to 'none' for the first set and is required for any later
140
+ * set. `authenticate()` sets it from the ARC validation result
141
+ */
142
+ cv?: 'none' | 'pass' | 'fail' | string;
143
+
144
+ /**
145
+ * ARC instance (i= tag), 1 to 50. Defaults to one more than the highest instance on the
146
+ * message. An instance that already exists is refused, and one that leaves a gap is
147
+ * refused in strict mode (the default mode seals it and adds an `arc-instance-gap` warning)
148
+ */
149
+ i?: number;
150
+
151
+ /**
152
+ * Authentication-Results payload for the ARC-Authentication-Results header (the part after
153
+ * "i=N;"). `authenticate()` sets it from its own results
154
+ */
155
+ authResults?: string;
156
+
157
+ /**
158
+ * Header fields to include in the ARC-Message-Signature, as an array or a colon separated
159
+ * string. Defaults to DKIM-Signature and the default DKIM header list. ARC header fields and
160
+ * Authentication-Results are never included (RFC 8617 section 4.1.2)
121
161
  */
122
- headerList?: string[];
162
+ headerList?: string[] | string;
123
163
 
124
164
  /**
125
165
  * Signing timestamp
126
166
  */
127
167
  signTime?: Date | string | number;
168
+
169
+ /**
170
+ * Follow the RFCs exactly instead of the lenient defaults (default: false)
171
+ */
172
+ strict?: boolean;
128
173
  }
129
174
 
130
175
  /**
@@ -132,7 +177,8 @@ export interface ARCSealOptions {
132
177
  */
133
178
  export interface AuthPolicy {
134
179
  /**
135
- * DKIM policy rules (e.g., 'weak-key' when key is undersized)
180
+ * DKIM policy rules: 'weak-key' when the key is shorter than minBitLength, 'weak-algorithm'
181
+ * for an rsa-sha1 signature in strict mode
136
182
  */
137
183
  'dkim-rules'?: string;
138
184
 
@@ -146,7 +192,10 @@ export interface AuthPolicy {
146
192
  * Status result from authentication checks
147
193
  */
148
194
  export interface AuthStatus {
149
- result: 'pass' | 'fail' | 'neutral' | 'none' | 'temperror' | 'temperr' | 'permerror' | 'policy' | 'softfail' | 'skipped';
195
+ /**
196
+ * The result keyword. `temperr` is deprecated, BIMI reports `temperror` now
197
+ */
198
+ result: 'pass' | 'fail' | 'neutral' | 'none' | 'temperror' | 'temperr' | 'permerror' | 'policy' | 'softfail' | 'skipped' | 'declined';
150
199
  comment?: string;
151
200
  header?: Record<string, any>;
152
201
  smtp?: {
@@ -156,25 +205,94 @@ export interface AuthStatus {
156
205
  policy?: AuthPolicy;
157
206
  }
158
207
 
208
+ /**
209
+ * Lax acceptance markers of a DKIM result: what the default mode accepted and strict mode
210
+ * would reject
211
+ *
212
+ * - `rsa-sha1`: the signature uses rsa-sha1 (RFC 8301 section 3.1)
213
+ * - `tag-syntax`: the signature is not valid tag-list syntax (duplicate tags, upper case tag
214
+ * names, a malformed t=, x= or l= value, or a d= or s= that is not a domain name)
215
+ * - `missing-v`, `missing-h` (and the other `missing-*`): a required tag is missing
216
+ * - `invalid-v`: v= is not "1"
217
+ * - `identity-domain`: the i= domain is not d= or its subdomain, or it is a subdomain while
218
+ * the key has t=s
219
+ * - `invalid-expiration`: x= is equal to t=
220
+ * - `query-method`: q= does not list dns/txt
221
+ * - `key-syntax`: the key record is not valid tag-list syntax
222
+ * - `key-v-syntax`: the key record v= is not the first tag, or not exactly "DKIM1"
223
+ * - `key-type-inferred`: an ed25519 key was found from its length, without k=ed25519
224
+ */
225
+ export type DKIMWarning =
226
+ | 'rsa-sha1'
227
+ | 'tag-syntax'
228
+ | 'missing-v'
229
+ | 'missing-a'
230
+ | 'missing-b'
231
+ | 'missing-bh'
232
+ | 'missing-d'
233
+ | 'missing-h'
234
+ | 'missing-s'
235
+ | 'invalid-v'
236
+ | 'identity-domain'
237
+ | 'invalid-expiration'
238
+ | 'query-method'
239
+ | 'key-syntax'
240
+ | 'key-v-syntax'
241
+ | 'key-type-inferred';
242
+
159
243
  /**
160
244
  * DKIM verification result for a single signature
161
245
  */
162
246
  export interface DKIMResult {
163
247
  /**
164
- * Signature identifier
248
+ * Signature identifier, the sha256 hash of the signature value
165
249
  */
166
250
  id?: string;
167
251
 
168
252
  /**
169
- * Signing domain
253
+ * Signing domain (d= tag)
170
254
  */
171
- signingDomain: string;
255
+ signingDomain?: string;
172
256
 
173
257
  /**
174
- * Key selector
258
+ * Key selector (s= tag)
175
259
  */
176
260
  selector?: string;
177
261
 
262
+ /**
263
+ * Signature value (b= tag)
264
+ */
265
+ signature?: string;
266
+
267
+ /**
268
+ * Signature algorithm (a= tag), eg. 'rsa-sha256'
269
+ */
270
+ algo?: string;
271
+
272
+ /**
273
+ * Canonicalization (c= tag), eg. 'relaxed/relaxed'
274
+ */
275
+ format?: string;
276
+
277
+ /**
278
+ * Body hash calculated for the message
279
+ */
280
+ bodyHash?: string;
281
+
282
+ /**
283
+ * Body hash from the bh= tag
284
+ */
285
+ bodyHashExpecting?: string;
286
+
287
+ /**
288
+ * Signed header fields and the canonicalized header data (base64)
289
+ */
290
+ signingHeaders?: {
291
+ keys: string;
292
+ headers: string[];
293
+ canonicalizedHeader: string;
294
+ };
295
+
178
296
  /**
179
297
  * Verification status
180
298
  */
@@ -190,6 +308,18 @@ export interface DKIMResult {
190
308
  * Number of body bytes left unsigned by an l= tag
191
309
  */
192
310
  underSized?: number;
311
+
312
+ /**
313
+ * True when the key record has the t=y flag, the signing domain is testing DKIM
314
+ * (RFC 6376 section 3.6.1)
315
+ */
316
+ testing?: boolean;
317
+
318
+ /**
319
+ * What the default mode accepted and strict mode would reject. Only present when
320
+ * there is something to report, never written into the Authentication-Results text
321
+ */
322
+ warnings?: DKIMWarning[];
193
323
  };
194
324
 
195
325
  /**
@@ -198,22 +328,82 @@ export interface DKIMResult {
198
328
  info: string;
199
329
 
200
330
  /**
201
- * Signature algorithm
331
+ * Signing time from the t= tag, as an ISO string
332
+ */
333
+ signTime?: string | null;
334
+
335
+ /**
336
+ * Expiration time from the x= tag, as an ISO string
337
+ */
338
+ expiresAfter?: string | null;
339
+
340
+ /**
341
+ * False when t= is in the future or x= has passed
342
+ */
343
+ signatureTimeValid?: boolean;
344
+
345
+ /**
346
+ * Size of the message body in bytes
347
+ */
348
+ sourceBodyLength?: number;
349
+
350
+ /**
351
+ * Number of canonicalized body bytes covered by the signature
352
+ */
353
+ canonBodyLength?: number;
354
+
355
+ /**
356
+ * Number of canonicalized body bytes in total
357
+ */
358
+ canonBodyLengthTotal?: number;
359
+
360
+ /**
361
+ * True when the signature has an l= tag
362
+ */
363
+ canonBodyLengthLimited?: boolean;
364
+
365
+ /**
366
+ * The l= value
367
+ */
368
+ canonBodyLengthLimit?: number;
369
+
370
+ /**
371
+ * Where the MIME structure starts in the body, for multipart messages
372
+ */
373
+ mimeStructureStart?: number;
374
+
375
+ /**
376
+ * Public key in PEM format
377
+ */
378
+ publicKey?: string;
379
+
380
+ /**
381
+ * RSA key size in bits
382
+ */
383
+ modulusLength?: number;
384
+
385
+ /**
386
+ * Key record as found in DNS
387
+ */
388
+ rr?: string;
389
+
390
+ /**
391
+ * @deprecated Never set, use `algo`
202
392
  */
203
393
  algorithm?: string;
204
394
 
205
395
  /**
206
- * Canonicalization method
396
+ * @deprecated Never set, use `format`
207
397
  */
208
398
  canonicalization?: string;
209
399
 
210
400
  /**
211
- * Signing timestamp
401
+ * @deprecated Never set, use `signTime`
212
402
  */
213
403
  signingTime?: Date;
214
404
 
215
405
  /**
216
- * Signature expiration
406
+ * @deprecated Never set, use `expiresAfter`
217
407
  */
218
408
  expiration?: Date;
219
409
  }
@@ -223,10 +413,16 @@ export interface DKIMResult {
223
413
  */
224
414
  export interface DKIMVerifyResult {
225
415
  /**
226
- * Domain from From header
416
+ * Addresses from the From header, as full addr-specs (the domain is what follows the last
417
+ * "@"). The mailboxes of a group (RFC 6854) are included
227
418
  */
228
419
  headerFrom: string[];
229
420
 
421
+ /**
422
+ * Number of From header fields in the message. RFC 5322 allows only one
423
+ */
424
+ fromFields: number;
425
+
230
426
  /**
231
427
  * Domain from Return-Path header
232
428
  */
@@ -242,8 +438,8 @@ export interface DKIMVerifyResult {
242
438
  * Access with result.headers or Object.getOwnPropertyDescriptor()
243
439
  */
244
440
  readonly headers?: {
245
- parsed: Array<{ key: string; line: string }>;
246
- [key: string]: any;
441
+ parsed: Array<{ key: string; casedKey: string; line: Buffer }>;
442
+ original: Buffer;
247
443
  };
248
444
 
249
445
  /**
@@ -312,6 +508,18 @@ export interface SPFResult {
312
508
  void: number;
313
509
  subqueries: Record<string, number>;
314
510
  };
511
+
512
+ /**
513
+ * Explanation string published by the domain owner with the "exp" modifier
514
+ * (RFC 7208 section 6.2). Only set for a "fail" result. This is third party text
515
+ */
516
+ explanation?: string;
517
+
518
+ /**
519
+ * Set in the default mode when something was accepted that strict mode would have
520
+ * rejected: "syntax-error", "void-lookup-limit" or "non-ascii-local-part"
521
+ */
522
+ warnings?: string[];
315
523
  }
316
524
 
317
525
  /**
@@ -354,7 +562,18 @@ export interface ARCResult {
354
562
  * Why sealing failed, when a seal was requested but could not be created.
355
563
  * The message is left unsealed in that case
356
564
  */
357
- sealErrors?: Error[];
565
+ sealErrors?: ARCSealError[];
566
+
567
+ /**
568
+ * What the default mode accepted in the chain that strict mode would reject, for example
569
+ * 'arc-tag-syntax' (a tag-list syntax error or a tag name that is not lower case),
570
+ * 'arc-instance-syntax' (an i= value that is not 1*2DIGIT, an ARC-Authentication-Results
571
+ * whose i= is not first, or an ARC header field that was ignored for its i= value),
572
+ * 'ams-c-default' (an ARC-Message-Signature without c= that only verified with
573
+ * relaxed/relaxed), and the key record warnings of the DKIM result. Not set for a failing
574
+ * chain, and never written into the headers
575
+ */
576
+ warnings?: string[];
358
577
 
359
578
  /**
360
579
  * ARC chain entries (non-enumerable property)
@@ -373,10 +592,16 @@ export interface DMARCResult {
373
592
  domain: string;
374
593
 
375
594
  /**
376
- * DMARC policy ('none', 'quarantine', 'reject')
595
+ * Effective DMARC policy ('none', 'quarantine', 'reject'): p for the author domain's own
596
+ * record, np or sp for an inherited one, one level lower with t=y
377
597
  */
378
598
  policy: string;
379
599
 
600
+ /**
601
+ * Domain whose DMARC record the policy was taken from
602
+ */
603
+ policyDomain?: string;
604
+
380
605
  /**
381
606
  * Policy for organizational domain
382
607
  */
@@ -388,9 +613,14 @@ export interface DMARCResult {
388
613
  sp: string;
389
614
 
390
615
  /**
391
- * Percentage of messages subject to filtering (0-100)
616
+ * Policy for non-existent subdomains, if published
392
617
  */
393
- pct?: number;
618
+ np?: string;
619
+
620
+ /**
621
+ * True when the record has t=y, in which case policy is one level below the published one
622
+ */
623
+ testMode: boolean;
394
624
 
395
625
  /**
396
626
  * DMARC DNS record
@@ -400,7 +630,26 @@ export interface DMARCResult {
400
630
  /**
401
631
  * Verification status
402
632
  */
403
- status: AuthStatus;
633
+ status: AuthStatus & {
634
+ header?: {
635
+ /**
636
+ * Author Domain
637
+ */
638
+ from?: string;
639
+
640
+ /**
641
+ * Domain of the DMARC record, omitted in strict mode (not a registered property for dmarc)
642
+ */
643
+ d?: string;
644
+ };
645
+
646
+ policy?: {
647
+ /**
648
+ * The policy that was applied (RFC 8601 section 2.7.2 policy.dmarc)
649
+ */
650
+ dmarc?: string;
651
+ };
652
+ };
404
653
 
405
654
  /**
406
655
  * Alignment results
@@ -438,6 +687,12 @@ export interface DMARCResult {
438
687
  * Error message if verification failed
439
688
  */
440
689
  error?: string;
690
+
691
+ /**
692
+ * What the default mode accepted and strict mode would reject, only present when there
693
+ * is something to report
694
+ */
695
+ warnings?: string[];
441
696
  }
442
697
 
443
698
  /**
@@ -445,9 +700,12 @@ export interface DMARCResult {
445
700
  */
446
701
  export interface BIMIResult {
447
702
  /**
448
- * Verification status
703
+ * Verification status. `declined` means the domain published a declination record,
704
+ * `temperror` a DNS failure. `temperr` is deprecated and no longer reported
449
705
  */
450
- status: AuthStatus;
706
+ status: AuthStatus & {
707
+ result: 'pass' | 'none' | 'fail' | 'skipped' | 'declined' | 'temperror' | 'temperr';
708
+ };
451
709
 
452
710
  /**
453
711
  * BIMI DNS record
@@ -484,7 +742,8 @@ export interface ReceivedChainEntry {
484
742
  value: string;
485
743
 
486
744
  /**
487
- * Additional comment (often contains IP in parentheses)
745
+ * The comments after the value, joined with spaces. Usually the TCP-info of
746
+ * RFC 5321 section 4.4, whose last address literal is the connecting IP address
488
747
  */
489
748
  comment?: string;
490
749
  };
@@ -581,6 +840,13 @@ export interface AuthenticateResult {
581
840
  * Combined authentication headers to prepend to message
582
841
  */
583
842
  headers: string;
843
+
844
+ /**
845
+ * Why DMARC was not evaluated when `dmarc` is false because the From header has no
846
+ * domain or more than one (RFC 9989 section 5.3.1). Such a message can be treated as
847
+ * suspicious (RFC 9989 section 11.5)
848
+ */
849
+ dmarcSkipReason?: 'no-author-domain' | 'multiple-author-domains' | 'invalid-author-domain' | 'multiple-from-fields';
584
850
  }
585
851
 
586
852
  /**
@@ -622,16 +888,17 @@ export interface DKIMSignOptions {
622
888
  canonicalization?: string;
623
889
 
624
890
  /**
625
- * Signing algorithm (default: 'rsa-sha256')
626
- * Supported: 'rsa-sha256', 'rsa-sha1', 'ed25519-sha256'
891
+ * Signing algorithm, defaults to 'rsa-sha256' or 'ed25519-sha256' depending on the key type.
892
+ * Supported: 'rsa-sha256', 'ed25519-sha256' and 'rsa-sha1'. rsa-sha1 is historic
893
+ * (RFC 8301): it adds the 'rsa-sha1' warning, and strict mode refuses it
627
894
  */
628
895
  algorithm?: string;
629
896
 
630
897
  /**
631
- * List of header fields to sign
632
- * Default includes From, Subject, Date, To, etc.
898
+ * Header fields to sign, as an array or a colon separated string. Default includes
899
+ * From, Subject, Date, To, etc. Set per signature in `signatureData` or for all of them
633
900
  */
634
- headerList?: string[];
901
+ headerList?: string[] | string;
635
902
 
636
903
  /**
637
904
  * Signing timestamp (defaults to current time)
@@ -649,7 +916,7 @@ export interface DKIMSignOptions {
649
916
  maxBodyLength?: number;
650
917
 
651
918
  /**
652
- * Identity (i= tag)
919
+ * Identity (i= tag), an address whose domain is the signing domain or its subdomain
653
920
  */
654
921
  identity?: string;
655
922
 
@@ -657,6 +924,15 @@ export interface DKIMSignOptions {
657
924
  * Multiple signature configurations
658
925
  */
659
926
  signatureData?: DKIMSignOptions[];
927
+
928
+ /**
929
+ * Follow the RFCs exactly (default: false). Strict mode refuses rsa-sha1, RSA keys
930
+ * shorter than 1024 bits, an expiration that is not after the signing time, a signing
931
+ * time that does not fit into t=, and a d=, s= or i= that is not valid RFC 6376 syntax.
932
+ * The default mode signs these and lists them in `warnings`. A d=, s= or identity that
933
+ * would break out of its tag is refused in both modes
934
+ */
935
+ strict?: boolean;
660
936
  }
661
937
 
662
938
  /**
@@ -829,12 +1105,43 @@ export interface ARCSigningData {
829
1105
  privateKey?: string | Buffer;
830
1106
  }
831
1107
 
1108
+ /**
1109
+ * A signature that could not be created
1110
+ */
1111
+ export interface DKIMSignError {
1112
+ /**
1113
+ * Why the signature was not created. `err.code` is eg. 'ENOFROM', 'EINVALIDALGO',
1114
+ * 'EINVALIDTYPE', 'ESHORTKEY', 'EINVALIDDOMAIN', 'EINVALIDSELECTOR', 'EINVALIDIDENTITY',
1115
+ * 'EINVALIDTIME', 'EINVALIDCANON' or 'EINVALIDINSTANCE'
1116
+ */
1117
+ err: Error & { code?: string };
1118
+
1119
+ type?: 'DKIM' | 'ARC';
1120
+ selector?: string;
1121
+ signingDomain?: string;
1122
+ algorithm?: string;
1123
+ canonicalization?: string;
1124
+ }
1125
+
1126
+ /**
1127
+ * What the default mode signed although strict mode would have refused it
1128
+ *
1129
+ * - `rsa-sha1`: signed with rsa-sha1
1130
+ * - `weak-key`: signed with an RSA key shorter than 1024 bits
1131
+ * - `invalid-expiration`: the expiration is not after the signing time, or does not fit into x=
1132
+ * - `invalid-signtime`: the signing time does not fit into t=, which was left out
1133
+ * - `d-syntax`, `s-syntax`: d= or s= is not valid RFC 6376 syntax
1134
+ * - `identity-domain`: the identity domain is not the signing domain or its subdomain
1135
+ */
1136
+ export type DKIMSignWarning = 'rsa-sha1' | 'weak-key' | 'invalid-expiration' | 'invalid-signtime' | 'd-syntax' | 's-syntax' | 'identity-domain';
1137
+
832
1138
  /**
833
1139
  * DKIM signing result
834
1140
  */
835
1141
  export interface DKIMSignResult {
836
1142
  /**
837
- * DKIM-Signature header(s) to prepend to message
1143
+ * DKIM-Signature header(s) to prepend to message, each ending with a CRLF. An empty
1144
+ * string when no signature was created
838
1145
  */
839
1146
  signatures: string;
840
1147
 
@@ -844,9 +1151,14 @@ export interface DKIMSignResult {
844
1151
  arc?: ARCSigningData;
845
1152
 
846
1153
  /**
847
- * Any errors encountered during signing
1154
+ * The signatures that could not be created, and why
1155
+ */
1156
+ errors: DKIMSignError[];
1157
+
1158
+ /**
1159
+ * What was signed although strict mode would have refused it
848
1160
  */
849
- errors: Error[];
1161
+ warnings: DKIMSignWarning[];
850
1162
  }
851
1163
 
852
1164
  /**
@@ -871,9 +1183,14 @@ export class DkimSignStream extends Transform {
871
1183
  constructor(options: DKIMSignOptions);
872
1184
 
873
1185
  /**
874
- * Any errors encountered during signing
1186
+ * The signatures that could not be created, set when the stream ends
875
1187
  */
876
- errors: Error[] | null;
1188
+ errors: DKIMSignError[] | null;
1189
+
1190
+ /**
1191
+ * What was signed although strict mode would have refused it, set when the stream ends
1192
+ */
1193
+ warnings: DKIMSignWarning[] | null;
877
1194
  }
878
1195
 
879
1196
  // ============================================================================
@@ -908,6 +1225,12 @@ export interface DKIMVerifyOptions {
908
1225
  * ARC sealing options (if sealing should be prepared)
909
1226
  */
910
1227
  seal?: ARCSealOptions;
1228
+
1229
+ /**
1230
+ * Follow RFC 6376, RFC 8301 and RFC 8463 exactly instead of the lenient default
1231
+ * (default: false). See `DKIMWarning` for what the default mode accepts
1232
+ */
1233
+ strict?: boolean;
911
1234
  }
912
1235
 
913
1236
  /**
@@ -961,6 +1284,17 @@ export interface SPFOptions {
961
1284
  * Custom DNS resolver function
962
1285
  */
963
1286
  resolver?: DNSResolver;
1287
+
1288
+ /**
1289
+ * Follow RFC 7208 exactly instead of the lenient default (default: false)
1290
+ */
1291
+ strict?: boolean;
1292
+
1293
+ /**
1294
+ * Maximum time in milliseconds for the whole evaluation, after which the result is
1295
+ * temperror (RFC 7208 section 4.6.4). No limit by default
1296
+ */
1297
+ maxElapsedTime?: number;
964
1298
  }
965
1299
 
966
1300
  /**
@@ -984,6 +1318,12 @@ export interface DMARCOptions {
984
1318
  */
985
1319
  headerFrom: string | string[];
986
1320
 
1321
+ /**
1322
+ * Number of From header fields the addresses came from. With more than one, DMARC
1323
+ * validation is not possible (reason "multiple-from-fields")
1324
+ */
1325
+ fromFields?: number;
1326
+
987
1327
  /**
988
1328
  * Domains that passed SPF
989
1329
  */
@@ -1008,6 +1348,11 @@ export interface DMARCOptions {
1008
1348
  * Custom DNS resolver function
1009
1349
  */
1010
1350
  resolver?: DNSResolver;
1351
+
1352
+ /**
1353
+ * Follow RFC 9989 exactly instead of the lenient default (default: false)
1354
+ */
1355
+ strict?: boolean;
1011
1356
  }
1012
1357
 
1013
1358
  /**
@@ -1042,6 +1387,24 @@ export interface ARCData {
1042
1387
  error?: Error;
1043
1388
  }
1044
1389
 
1390
+ /**
1391
+ * An error from ARC sealing
1392
+ */
1393
+ export interface ARCSealError {
1394
+ /**
1395
+ * The error. Codes include EINVALIDCV (a missing or invalid cv), EINVALIDINSTANCE (an
1396
+ * instance out of 1-50, a chain that already has 50 sets, one that already exists, or in
1397
+ * strict mode one that leaves a gap),
1398
+ * EARCCHAINFAILED (the newest ARC-Seal already has cv=fail, RFC 8617 section 5.1 step 2),
1399
+ * EINVALIDALGO and EINVALIDTYPE (an algorithm that is not supported or does not match the key)
1400
+ */
1401
+ err: Error & { code?: string };
1402
+ type?: string;
1403
+ selector?: string;
1404
+ signingDomain?: string;
1405
+ [key: string]: any;
1406
+ }
1407
+
1045
1408
  /**
1046
1409
  * ARC verification options
1047
1410
  */
@@ -1055,6 +1418,47 @@ export interface ARCOptions {
1055
1418
  * Minimal allowed public key length in bits (default: 1024)
1056
1419
  */
1057
1420
  minBitLength?: number;
1421
+
1422
+ /**
1423
+ * Follow RFC 8617 exactly instead of the lenient default (default: false). In strict mode
1424
+ * ARC-Seal and ARC-Message-Signature tag lists must be valid RFC 6376 section 3.2 syntax
1425
+ * with lower case tag names and every required tag, instance values must be 1*2DIGIT from
1426
+ * 1 to 50 (and come first in ARC-Authentication-Results), ARC header fields with an
1427
+ * invalid instance fail the chain instead of being ignored, an ARC-Message-Signature
1428
+ * without c= is only verified as simple/simple, and the arc= result also reports
1429
+ * arc=none and smtp.remote-ip
1430
+ */
1431
+ strict?: boolean;
1432
+
1433
+ /**
1434
+ * Client IP address, reported as smtp.remote-ip in strict mode (RFC 8617 section 6)
1435
+ */
1436
+ ip?: string;
1437
+ }
1438
+
1439
+ /**
1440
+ * Seal creation data for createSeal()
1441
+ */
1442
+ export interface ARCCreateSealData {
1443
+ /**
1444
+ * Parsed message headers, when `input` is false
1445
+ */
1446
+ headers?: any;
1447
+
1448
+ /**
1449
+ * ARC chain data of the message, when `input` is false
1450
+ */
1451
+ arc?: ARCData | ARCSigningData;
1452
+
1453
+ /**
1454
+ * Sealing options, with `bodyHash` (the relaxed/relaxed sha256 body hash) when `input` is false
1455
+ */
1456
+ seal: ARCSealOptions & { bodyHash?: string };
1457
+
1458
+ /**
1459
+ * Follow the RFCs exactly (default: false, or `seal.strict`)
1460
+ */
1461
+ strict?: boolean;
1058
1462
  }
1059
1463
 
1060
1464
  /**
@@ -1076,30 +1480,34 @@ export function arc(data: ARCData, opts?: ARCOptions): Promise<ARCResult>;
1076
1480
  export function sealMessage(input: MessageInput, seal: ARCSealOptions): Promise<Buffer>;
1077
1481
 
1078
1482
  /**
1079
- * Gets ARC chain from parsed headers
1483
+ * Gets ARC chain from parsed headers and validates its structure. Throws for a chain that is not valid
1080
1484
  *
1081
1485
  * @param headers - Parsed message headers
1486
+ * @param opts - `strict` applies the RFC 8617 syntax rules, lenient acceptances are added to `warnings`
1082
1487
  * @returns ARC chain or false if no chain found
1083
1488
  */
1084
- export function getARChain(headers: any): ARCChainEntry[] | false;
1489
+ export function getARChain(headers: any, opts?: { strict?: boolean; warnings?: string[] }): ARCChainEntry[] | false;
1085
1490
 
1086
1491
  /**
1087
- * Verifies ARC seal chain
1492
+ * Verifies ARC seal chain. Throws if any seal of the chain is not valid, including a chain whose
1493
+ * newest seal has cv=fail
1088
1494
  *
1089
1495
  * @param data - ARC chain data
1090
1496
  * @param opts - ARC verification options
1091
- * @returns true if chain is valid
1497
+ * @returns true if chain is valid, false if there is no chain
1092
1498
  */
1093
- export function verifyASChain(data: ARCData, opts: ARCOptions): Promise<boolean>;
1499
+ export function verifyASChain(data: ARCData, opts: ARCOptions & { warnings?: string[] }): Promise<boolean>;
1094
1500
 
1095
1501
  /**
1096
1502
  * Creates ARC seal headers
1097
1503
  *
1098
1504
  * @param input - RFC822 formatted message or false for pre-calculated data
1099
1505
  * @param data - Seal creation data
1100
- * @returns Seal headers, empty if the ARC-Message-Signature could not be signed, and any errors
1506
+ * @returns Seal headers (empty when no set was created), the errors that say why, and what the
1507
+ * default mode sealed that strict mode would have refused ('arc-instance-gap',
1508
+ * 'arc-cv-instance', and the DKIM signing warnings)
1101
1509
  */
1102
- export function createSeal(input: MessageInput | false, data: any): Promise<{ headers: string[]; errors: Error[] }>;
1510
+ export function createSeal(input: MessageInput | false, data: ARCCreateSealData): Promise<{ headers: string[]; errors: ARCSealError[]; warnings: string[] }>;
1103
1511
 
1104
1512
  // ============================================================================
1105
1513
  // BIMI
@@ -1128,6 +1536,12 @@ export interface BIMIOptions {
1128
1536
  * Custom DNS resolver function
1129
1537
  */
1130
1538
  resolver?: DNSResolver;
1539
+
1540
+ /**
1541
+ * Format the Authentication-Results entry in the RFC 8601 section 2.2 form (default: false).
1542
+ * BIMI has no other strict-only rules, the draft checks apply in both modes
1543
+ */
1544
+ strict?: boolean;
1131
1545
  }
1132
1546
 
1133
1547
  /**
@@ -1338,10 +1752,33 @@ export interface MTASTSPolicyResult {
1338
1752
 
1339
1753
  /**
1340
1754
  * Status of the fetch operation
1755
+ *
1756
+ * - `found`: a new or changed policy was fetched
1757
+ * - `renewed`: the cached policy has the same id and has not expired, it is returned without a fetch
1758
+ * - `not_found`: the domain has no MTA-STS policy
1759
+ * - `errored`: the policy could not be fetched. With a valid cached policy, that policy is
1760
+ * returned with `error` set
1341
1761
  */
1342
1762
  status: 'found' | 'renewed' | 'not_found' | 'errored';
1763
+
1764
+ /**
1765
+ * Lax acceptance markers, only present when the default lax mode accepted something strict mode would reject
1766
+ */
1767
+ warnings?: MTASTSWarning[];
1343
1768
  }
1344
1769
 
1770
+ /**
1771
+ * Markers for input that the default lax mode accepted but `strict: true` would reject
1772
+ *
1773
+ * - `txt-syntax`: the TXT record does not match the RFC 8461 3.1 syntax
1774
+ * - `http-status`: the policy was served with a 2xx status other than 200
1775
+ * - `content-type`: the policy was not served as `text/plain`
1776
+ * - `policy-syntax`: the policy file does not match the RFC 8461 3.2 syntax
1777
+ * - `cert-identity`: the Policy Host certificate matched only through the CN or a partial-label wildcard
1778
+ * - `expired-cache`: an expired cached policy was returned because the policy could not be fetched
1779
+ */
1780
+ export type MTASTSWarning = 'txt-syntax' | 'http-status' | 'content-type' | 'policy-syntax' | 'cert-identity' | 'expired-cache';
1781
+
1345
1782
  /**
1346
1783
  * MTA-STS MX validation result
1347
1784
  */
@@ -1375,6 +1812,21 @@ export interface MTASTSOptions {
1375
1812
  * Custom DNS resolver function
1376
1813
  */
1377
1814
  resolver?: DNSResolver;
1815
+
1816
+ /**
1817
+ * Apply the RFC 8461 rules exactly (default false)
1818
+ */
1819
+ strict?: boolean;
1820
+
1821
+ /**
1822
+ * Overall time limit for the HTTPS policy request in milliseconds (default 60000)
1823
+ */
1824
+ timeout?: number;
1825
+
1826
+ /**
1827
+ * Maximum size of the policy file in bytes (default 65536)
1828
+ */
1829
+ maxPolicySize?: number;
1378
1830
  }
1379
1831
 
1380
1832
  // Note: MTA-STS functions (resolvePolicy, fetchPolicy, parsePolicy, validateMx, getPolicy)