mailauth 7.1.0 → 7.2.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +60 -0
- package/README.md +32 -31
- package/cli.md +1 -1
- package/docs/arc.md +62 -54
- package/docs/bimi.md +153 -54
- package/docs/dkim.md +37 -36
- package/docs/dmarc.md +4 -1
- package/docs/mta-sts.md +12 -9
- package/docs/spf.md +14 -13
- package/index.d.ts +224 -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/report.js +2 -2
- package/lib/commands/seal.js +8 -6
- package/lib/commands/spf.js +2 -2
- 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/syntax.js +11 -0
- package/lib/dkim/verify.js +1 -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 +17 -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 +159 -21
- package/package.json +18 -9
- package/stryker.config.json +16 -0
package/index.d.ts
CHANGED
|
@@ -144,20 +144,22 @@ export interface ARCSealOptions {
|
|
|
144
144
|
/**
|
|
145
145
|
* Chain validation status for the ARC-Seal cv= tag: 'none', 'pass' or 'fail' (compared
|
|
146
146
|
* case-insensitively). Defaults to 'none' for the first set and is required for any later
|
|
147
|
-
* set. `authenticate()` sets it from the ARC validation result
|
|
147
|
+
* set. `authenticate()` always sets it from the ARC validation result and ignores this value
|
|
148
148
|
*/
|
|
149
149
|
cv?: 'none' | 'pass' | 'fail' | string;
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* ARC instance (i= tag), 1 to 50. Defaults to one more than the highest instance on the
|
|
153
153
|
* message. An instance that already exists is refused, and one that leaves a gap is
|
|
154
|
-
* refused in strict mode (the default mode seals it and adds an `arc-instance-gap` warning)
|
|
154
|
+
* refused in strict mode (the default mode seals it and adds an `arc-instance-gap` warning).
|
|
155
|
+
* `authenticate()` ignores it and always uses the next instance
|
|
155
156
|
*/
|
|
156
157
|
i?: number;
|
|
157
158
|
|
|
158
159
|
/**
|
|
159
160
|
* Authentication-Results payload for the ARC-Authentication-Results header (the part after
|
|
160
|
-
* "i=N;").
|
|
161
|
+
* "i=N;"). Required, and a line break is only allowed as folding (CRLF followed by a space or
|
|
162
|
+
* tab). `authenticate()` always sets it from its own results and ignores this value
|
|
161
163
|
*/
|
|
162
164
|
authResults?: string;
|
|
163
165
|
|
|
@@ -218,16 +220,20 @@ export interface AuthStatus {
|
|
|
218
220
|
*
|
|
219
221
|
* - `rsa-sha1`: the signature uses rsa-sha1 (RFC 8301 section 3.1)
|
|
220
222
|
* - `tag-syntax`: the signature is not valid tag-list syntax (duplicate tags, upper case tag
|
|
221
|
-
* names, a malformed t=, x= or l= value,
|
|
223
|
+
* names, a malformed c=, t=, x= or l= value, an l= larger than the canonicalized body, or a d=
|
|
224
|
+
* or s= that is not a domain name)
|
|
222
225
|
* - `missing-v`, `missing-h` (and the other `missing-*`): a required tag is missing
|
|
223
226
|
* - `invalid-v`: v= is not "1"
|
|
224
227
|
* - `identity-domain`: the i= domain is not d= or its subdomain, or it is a subdomain while
|
|
225
228
|
* the key has t=s
|
|
226
229
|
* - `invalid-expiration`: x= is equal to t=
|
|
227
230
|
* - `query-method`: q= does not list dns/txt
|
|
228
|
-
* - `key-syntax`: the key record is not valid tag-list syntax
|
|
231
|
+
* - `key-syntax`: the key record is not valid tag-list syntax, or a k=, s= or t= value has
|
|
232
|
+
* whitespace inside it
|
|
229
233
|
* - `key-v-syntax`: the key record v= is not the first tag, or not exactly "DKIM1"
|
|
230
234
|
* - `key-type-inferred`: an ed25519 key was found from its length, without k=ed25519
|
|
235
|
+
* - `key-ed25519-spki`: an ed25519 key was published as a SubjectPublicKeyInfo structure, not
|
|
236
|
+
* as the bare 32 octet key of RFC 8463 section 4.2
|
|
231
237
|
*/
|
|
232
238
|
export type DKIMWarning =
|
|
233
239
|
| 'rsa-sha1'
|
|
@@ -245,7 +251,8 @@ export type DKIMWarning =
|
|
|
245
251
|
| 'query-method'
|
|
246
252
|
| 'key-syntax'
|
|
247
253
|
| 'key-v-syntax'
|
|
248
|
-
| 'key-type-inferred'
|
|
254
|
+
| 'key-type-inferred'
|
|
255
|
+
| 'key-ed25519-spki';
|
|
249
256
|
|
|
250
257
|
/**
|
|
251
258
|
* DKIM verification result for a single signature
|
|
@@ -307,7 +314,8 @@ export interface DKIMResult {
|
|
|
307
314
|
/**
|
|
308
315
|
* Signing domain if it aligns with the From domain, false otherwise. For a passing
|
|
309
316
|
* signature this follows the DMARC verdict once DMARC found a record, before that it
|
|
310
|
-
* only guesses relaxed alignment from the Public Suffix List.
|
|
317
|
+
* only guesses relaxed alignment from the Public Suffix List. Always false when the From
|
|
318
|
+
* header field does not yield a single Author Domain, as DMARC requires.
|
|
311
319
|
*/
|
|
312
320
|
aligned?: string | false;
|
|
313
321
|
|
|
@@ -430,6 +438,14 @@ export interface DKIMVerifyResult {
|
|
|
430
438
|
*/
|
|
431
439
|
fromFields: number;
|
|
432
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Syntax of the From header fields: "valid" (RFC 5322), "lax" (accepted only by the lenient
|
|
443
|
+
* parsing, such as an unquoted comma in a display name) or "invalid" (no reliable addresses,
|
|
444
|
+
* for example text after the angle-addr). DMARC is not evaluated for an "invalid" field, nor
|
|
445
|
+
* for a "lax" one in strict mode
|
|
446
|
+
*/
|
|
447
|
+
fromSyntax: 'valid' | 'lax' | 'invalid';
|
|
448
|
+
|
|
433
449
|
/**
|
|
434
450
|
* Domain from Return-Path header
|
|
435
451
|
*/
|
|
@@ -542,6 +558,10 @@ export interface ARCResult {
|
|
|
542
558
|
* Verification status
|
|
543
559
|
*/
|
|
544
560
|
status: AuthStatus & {
|
|
561
|
+
/**
|
|
562
|
+
* Set for a failed chain: whether it is sealed with cv=fail, false when the newest
|
|
563
|
+
* ARC-Seal on the message already says cv=fail (RFC 8617 section 5.1 step 2)
|
|
564
|
+
*/
|
|
545
565
|
shouldSeal?: boolean;
|
|
546
566
|
};
|
|
547
567
|
|
|
@@ -600,9 +620,11 @@ export interface DMARCResult {
|
|
|
600
620
|
|
|
601
621
|
/**
|
|
602
622
|
* Effective DMARC policy ('none', 'quarantine', 'reject'): p for the author domain's own
|
|
603
|
-
* record, np or sp for an inherited one, one level lower with t=y
|
|
623
|
+
* record, np or sp for an inherited one, one level lower with t=y. Like p, sp, testMode and
|
|
624
|
+
* alignment, only set when a policy record was applied, not for a none result or a
|
|
625
|
+
* temperror before a record was found
|
|
604
626
|
*/
|
|
605
|
-
policy
|
|
627
|
+
policy?: string;
|
|
606
628
|
|
|
607
629
|
/**
|
|
608
630
|
* Domain whose DMARC record the policy was taken from
|
|
@@ -612,12 +634,12 @@ export interface DMARCResult {
|
|
|
612
634
|
/**
|
|
613
635
|
* Policy for organizational domain
|
|
614
636
|
*/
|
|
615
|
-
p
|
|
637
|
+
p?: string;
|
|
616
638
|
|
|
617
639
|
/**
|
|
618
640
|
* Policy for subdomains
|
|
619
641
|
*/
|
|
620
|
-
sp
|
|
642
|
+
sp?: string;
|
|
621
643
|
|
|
622
644
|
/**
|
|
623
645
|
* Policy for non-existent subdomains, if published
|
|
@@ -627,7 +649,7 @@ export interface DMARCResult {
|
|
|
627
649
|
/**
|
|
628
650
|
* True when the record has t=y, in which case policy is one level below the published one
|
|
629
651
|
*/
|
|
630
|
-
testMode
|
|
652
|
+
testMode?: boolean;
|
|
631
653
|
|
|
632
654
|
/**
|
|
633
655
|
* DMARC DNS record
|
|
@@ -640,7 +662,7 @@ export interface DMARCResult {
|
|
|
640
662
|
status: AuthStatus & {
|
|
641
663
|
header?: {
|
|
642
664
|
/**
|
|
643
|
-
* Author Domain
|
|
665
|
+
* Author Domain (RFC 9989 section 9.1 header.from)
|
|
644
666
|
*/
|
|
645
667
|
from?: string;
|
|
646
668
|
|
|
@@ -652,16 +674,16 @@ export interface DMARCResult {
|
|
|
652
674
|
|
|
653
675
|
policy?: {
|
|
654
676
|
/**
|
|
655
|
-
* The policy that was applied (RFC
|
|
677
|
+
* The policy that was applied (RFC 9989 section 9.1 policy.dmarc)
|
|
656
678
|
*/
|
|
657
679
|
dmarc?: string;
|
|
658
680
|
};
|
|
659
681
|
};
|
|
660
682
|
|
|
661
683
|
/**
|
|
662
|
-
* Alignment results
|
|
684
|
+
* Alignment results, set when a policy record was applied
|
|
663
685
|
*/
|
|
664
|
-
alignment
|
|
686
|
+
alignment?: {
|
|
665
687
|
spf: {
|
|
666
688
|
/**
|
|
667
689
|
* Aligned SPF domain, or undefined if no domain aligned
|
|
@@ -729,6 +751,18 @@ export interface BIMIResult {
|
|
|
729
751
|
*/
|
|
730
752
|
authority?: string;
|
|
731
753
|
|
|
754
|
+
/**
|
|
755
|
+
* Avatar preference from the avp= tag, set only for the valid values
|
|
756
|
+
*/
|
|
757
|
+
preference?: 'personal' | 'brand';
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Assertion Record and BIMI-Selector syntax deviations that were accepted because `strict` was not set:
|
|
761
|
+
* 'record-version-case', 'record-tag-case', 'record-syntax', 'uri-comma', 'location-format',
|
|
762
|
+
* 'selector-version-case', 'selector-syntax'
|
|
763
|
+
*/
|
|
764
|
+
warnings?: string[];
|
|
765
|
+
|
|
732
766
|
/**
|
|
733
767
|
* Authentication-Results formatted info
|
|
734
768
|
*/
|
|
@@ -870,9 +904,9 @@ export function authenticate(input: MessageInput, opts?: AuthenticateOptions): P
|
|
|
870
904
|
// ============================================================================
|
|
871
905
|
|
|
872
906
|
/**
|
|
873
|
-
* DKIM
|
|
907
|
+
* A single DKIM signature to create, an entry of `DKIMSignOptions.signatureData`
|
|
874
908
|
*/
|
|
875
|
-
export interface
|
|
909
|
+
export interface DKIMSignatureData {
|
|
876
910
|
/**
|
|
877
911
|
* Signing domain (d= tag)
|
|
878
912
|
*/
|
|
@@ -884,10 +918,44 @@ export interface DKIMSignOptions {
|
|
|
884
918
|
selector: string;
|
|
885
919
|
|
|
886
920
|
/**
|
|
887
|
-
* Private key for signing (PEM format)
|
|
921
|
+
* Private key for signing (PEM format). An entry without a key is not signed and gets an
|
|
922
|
+
* `ENOKEY` error
|
|
888
923
|
*/
|
|
889
924
|
privateKey: string | Buffer;
|
|
890
925
|
|
|
926
|
+
/**
|
|
927
|
+
* Overrides the shared `canonicalization`
|
|
928
|
+
*/
|
|
929
|
+
canonicalization?: string;
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Overrides the shared `algorithm`
|
|
933
|
+
*/
|
|
934
|
+
algorithm?: string;
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Overrides the shared `headerList`
|
|
938
|
+
*/
|
|
939
|
+
headerList?: string[] | string;
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Maximum body length to sign (l= tag)
|
|
943
|
+
*/
|
|
944
|
+
maxBodyLength?: number;
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Identity (i= tag), an address whose domain is the signing domain or its subdomain.
|
|
948
|
+
* Given unencoded, it is written as dkim-quoted-printable (`jõgi@example.com` as
|
|
949
|
+
* `i=j=C3=B5gi@example.com`)
|
|
950
|
+
*/
|
|
951
|
+
identity?: string;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* DKIM signing options. The values of each signature are set in `signatureData`, the other
|
|
956
|
+
* options apply to all of them
|
|
957
|
+
*/
|
|
958
|
+
export interface DKIMSignOptions {
|
|
891
959
|
/**
|
|
892
960
|
* Canonicalization algorithm (default: 'relaxed/relaxed')
|
|
893
961
|
* Format: 'header/body' where each can be 'simple' or 'relaxed'
|
|
@@ -903,7 +971,9 @@ export interface DKIMSignOptions {
|
|
|
903
971
|
|
|
904
972
|
/**
|
|
905
973
|
* Header fields to sign, as an array or a colon separated string. Default includes
|
|
906
|
-
* From, Subject, Date, To, etc. Set per signature in `signatureData` or for all of them
|
|
974
|
+
* From, Subject, Date, To, etc. Set per signature in `signatureData` or for all of them.
|
|
975
|
+
* A name listed more than once is over-signed: it appears in h= as many times as it is
|
|
976
|
+
* listed or as the message has that field, whichever is more (`'From:From'`)
|
|
907
977
|
*/
|
|
908
978
|
headerList?: string[] | string;
|
|
909
979
|
|
|
@@ -918,19 +988,10 @@ export interface DKIMSignOptions {
|
|
|
918
988
|
expires?: Date | string | number;
|
|
919
989
|
|
|
920
990
|
/**
|
|
921
|
-
*
|
|
991
|
+
* The signatures to create. Without any, nothing is signed and `errors` holds an
|
|
992
|
+
* `ENOSIGNATURE` error
|
|
922
993
|
*/
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
/**
|
|
926
|
-
* Identity (i= tag), an address whose domain is the signing domain or its subdomain
|
|
927
|
-
*/
|
|
928
|
-
identity?: string;
|
|
929
|
-
|
|
930
|
-
/**
|
|
931
|
-
* Multiple signature configurations
|
|
932
|
-
*/
|
|
933
|
-
signatureData?: DKIMSignOptions[];
|
|
994
|
+
signatureData: DKIMSignatureData[];
|
|
934
995
|
|
|
935
996
|
/**
|
|
936
997
|
* Follow the RFCs exactly (default: false). Strict mode refuses rsa-sha1, RSA keys
|
|
@@ -1119,7 +1180,7 @@ export interface DKIMSignError {
|
|
|
1119
1180
|
/**
|
|
1120
1181
|
* Why the signature was not created. `err.code` is eg. 'ENOFROM', 'EINVALIDALGO',
|
|
1121
1182
|
* 'EINVALIDTYPE', 'ESHORTKEY', 'EINVALIDDOMAIN', 'EINVALIDSELECTOR', 'EINVALIDIDENTITY',
|
|
1122
|
-
* 'EINVALIDTIME', 'EINVALIDCANON' or '
|
|
1183
|
+
* 'EINVALIDTIME', 'EINVALIDCANON', 'EINVALIDINSTANCE', 'ENOKEY' or 'ENOSIGNATURE'
|
|
1123
1184
|
*/
|
|
1124
1185
|
err: Error & { code?: string };
|
|
1125
1186
|
|
|
@@ -1138,9 +1199,11 @@ export interface DKIMSignError {
|
|
|
1138
1199
|
* - `invalid-expiration`: the expiration is not after the signing time, or does not fit into x=
|
|
1139
1200
|
* - `invalid-signtime`: the signing time does not fit into t=, which was left out
|
|
1140
1201
|
* - `d-syntax`, `s-syntax`: d= or s= is not valid RFC 6376 syntax
|
|
1202
|
+
* - `identity-syntax`: the identity is not valid RFC 6376 i= syntax
|
|
1141
1203
|
* - `identity-domain`: the identity domain is not the signing domain or its subdomain
|
|
1142
1204
|
*/
|
|
1143
|
-
export type DKIMSignWarning =
|
|
1205
|
+
export type DKIMSignWarning =
|
|
1206
|
+
'rsa-sha1' | 'weak-key' | 'invalid-expiration' | 'invalid-signtime' | 'd-syntax' | 's-syntax' | 'identity-syntax' | 'identity-domain';
|
|
1144
1207
|
|
|
1145
1208
|
/**
|
|
1146
1209
|
* DKIM signing result
|
|
@@ -1337,6 +1400,13 @@ export interface DMARCOptions {
|
|
|
1337
1400
|
*/
|
|
1338
1401
|
fromFields?: number;
|
|
1339
1402
|
|
|
1403
|
+
/**
|
|
1404
|
+
* Syntax of the From header field the addresses came from (see DKIMVerifyResult.fromSyntax).
|
|
1405
|
+
* With "invalid", or "lax" in strict mode, DMARC validation is not possible (reason
|
|
1406
|
+
* "invalid-author-domain"). "lax" adds the "from-syntax" warning in the default mode
|
|
1407
|
+
*/
|
|
1408
|
+
fromSyntax?: 'valid' | 'lax' | 'invalid';
|
|
1409
|
+
|
|
1340
1410
|
/**
|
|
1341
1411
|
* Domains that passed SPF
|
|
1342
1412
|
*/
|
|
@@ -1398,6 +1468,13 @@ export interface ARCData {
|
|
|
1398
1468
|
* Error encountered during ARC chain parsing
|
|
1399
1469
|
*/
|
|
1400
1470
|
error?: Error;
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* The lower case cv= of the newest ARC-Seal in the header fields, or false if there is
|
|
1474
|
+
* none, set by the DKIM verifier. Read from the header fields even when the chain could not
|
|
1475
|
+
* be parsed, to decide whether a failed chain is sealed with cv=fail
|
|
1476
|
+
*/
|
|
1477
|
+
latestCv?: string | false;
|
|
1401
1478
|
}
|
|
1402
1479
|
|
|
1403
1480
|
/**
|
|
@@ -1409,7 +1486,9 @@ export interface ARCSealError {
|
|
|
1409
1486
|
* instance out of 1-50, a chain that already has 50 sets, one that already exists, or in
|
|
1410
1487
|
* strict mode one that leaves a gap),
|
|
1411
1488
|
* EARCCHAINFAILED (the newest ARC-Seal already has cv=fail, RFC 8617 section 5.1 step 2),
|
|
1412
|
-
* EINVALIDALGO and EINVALIDTYPE (an algorithm that is not supported or does not match the key)
|
|
1489
|
+
* EINVALIDALGO and EINVALIDTYPE (an algorithm that is not supported or does not match the key),
|
|
1490
|
+
* EINVALIDAUTHRESULTS (a missing or empty authResults, or one with a line break that is not
|
|
1491
|
+
* CRLF folding)
|
|
1413
1492
|
*/
|
|
1414
1493
|
err: Error & { code?: string };
|
|
1415
1494
|
type?: string;
|
|
@@ -1516,11 +1595,14 @@ export function verifyASChain(data: ARCData, opts: ARCOptions & { warnings?: str
|
|
|
1516
1595
|
*
|
|
1517
1596
|
* @param input - RFC822 formatted message or false for pre-calculated data
|
|
1518
1597
|
* @param data - Seal creation data
|
|
1519
|
-
* @returns Seal headers (empty when no set was created), the errors that say why,
|
|
1598
|
+
* @returns Seal headers (empty when no set was created), the errors that say why, what the
|
|
1520
1599
|
* default mode sealed that strict mode would have refused ('arc-instance-gap',
|
|
1521
|
-
* 'arc-cv-instance', and the DKIM signing warnings)
|
|
1600
|
+
* 'arc-cv-instance', and the DKIM signing warnings), and the instance (i=) of a created set
|
|
1522
1601
|
*/
|
|
1523
|
-
export function createSeal(
|
|
1602
|
+
export function createSeal(
|
|
1603
|
+
input: MessageInput | false,
|
|
1604
|
+
data: ARCCreateSealData
|
|
1605
|
+
): Promise<{ headers: string[]; errors: ARCSealError[]; warnings: string[]; instance?: number }>;
|
|
1524
1606
|
|
|
1525
1607
|
// ============================================================================
|
|
1526
1608
|
// BIMI
|
|
@@ -1551,8 +1633,10 @@ export interface BIMIOptions {
|
|
|
1551
1633
|
resolver?: DNSResolver;
|
|
1552
1634
|
|
|
1553
1635
|
/**
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
1636
|
+
* Parse the Assertion Record exactly (default: false): `v=BIMI1` and the tag names are
|
|
1637
|
+
* case-sensitive, syntax errors such as duplicate tags fail the record, and so do an `l=` URI with
|
|
1638
|
+
* another image format suffix and unencoded commas in URIs. Without it these are accepted and
|
|
1639
|
+
* reported in `warnings`. Also formats the Authentication-Results entry in the RFC 8601 section 2.2 form
|
|
1556
1640
|
*/
|
|
1557
1641
|
strict?: boolean;
|
|
1558
1642
|
}
|
|
@@ -1589,6 +1673,11 @@ export interface VMCValidationResult {
|
|
|
1589
1673
|
*/
|
|
1590
1674
|
code?: string;
|
|
1591
1675
|
|
|
1676
|
+
/**
|
|
1677
|
+
* Additional error details, for `SVG_VALIDATION_FAILED` the message and code of the SVG validation error
|
|
1678
|
+
*/
|
|
1679
|
+
details?: any;
|
|
1680
|
+
|
|
1592
1681
|
/**
|
|
1593
1682
|
* Redirect URL if logo location redirected
|
|
1594
1683
|
*/
|
|
@@ -1596,7 +1685,8 @@ export interface VMCValidationResult {
|
|
|
1596
1685
|
};
|
|
1597
1686
|
|
|
1598
1687
|
/**
|
|
1599
|
-
* Base64-encoded SVG logo file content
|
|
1688
|
+
* Base64-encoded SVG logo file content, uncompressed if the file was SVGZ. Set only when the
|
|
1689
|
+
* file passed SVG validation
|
|
1600
1690
|
*/
|
|
1601
1691
|
logoFile?: string;
|
|
1602
1692
|
|
|
@@ -1665,6 +1755,44 @@ export interface VMCValidationResult {
|
|
|
1665
1755
|
*/
|
|
1666
1756
|
hashMatch?: boolean;
|
|
1667
1757
|
};
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* The BIMI status of `bimiData` updated with the validation results (7.7): `fail` with a comment
|
|
1761
|
+
* when the indicator or the evidence document failed, `policy.authority` set to `pass` or `fail`
|
|
1762
|
+
* when an evidence document was checked. Set only when `bimiData.status.result` was `pass`
|
|
1763
|
+
*/
|
|
1764
|
+
status?: BIMIResult['status'];
|
|
1765
|
+
|
|
1766
|
+
/**
|
|
1767
|
+
* Authentication-Results entry for the updated status, use it instead of the `info` of `bimi()`
|
|
1768
|
+
*/
|
|
1769
|
+
info?: string;
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* SVG Tiny PS profile deviations of the logo files that were accepted because `strict` was not set,
|
|
1773
|
+
* for example 'svg-version'
|
|
1774
|
+
*/
|
|
1775
|
+
warnings?: string[];
|
|
1776
|
+
|
|
1777
|
+
/**
|
|
1778
|
+
* BIMI headers for the message, set only when the logo and the evidence document (if any) validated
|
|
1779
|
+
*/
|
|
1780
|
+
headers?: {
|
|
1781
|
+
/**
|
|
1782
|
+
* BIMI-Indicator header with the base64-encoded SVG logo
|
|
1783
|
+
*/
|
|
1784
|
+
indicator: string;
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* BIMI-Location header, with a= when the evidence document was verified
|
|
1788
|
+
*/
|
|
1789
|
+
location: string;
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* BIMI-Logo-Preference header
|
|
1793
|
+
*/
|
|
1794
|
+
preference?: string;
|
|
1795
|
+
};
|
|
1668
1796
|
}
|
|
1669
1797
|
|
|
1670
1798
|
/**
|
|
@@ -1679,6 +1807,33 @@ export interface BIMIData extends BIMIResult {
|
|
|
1679
1807
|
* VMC validation options
|
|
1680
1808
|
*/
|
|
1681
1809
|
export interface VMCValidationOptions {
|
|
1810
|
+
/**
|
|
1811
|
+
* Validate the logo files exactly against the SVG Tiny PS profile (default: false). Without it,
|
|
1812
|
+
* the deviations that do not make a logo unsafe are accepted and listed in `warnings`. Also
|
|
1813
|
+
* formats `info` in the RFC 8601 section 2.2 form
|
|
1814
|
+
*/
|
|
1815
|
+
strict?: boolean;
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Maximum size of the logo file in bytes, also applied to an uncompressed SVGZ file (default: 65536)
|
|
1819
|
+
*/
|
|
1820
|
+
maxLogoSize?: number;
|
|
1821
|
+
|
|
1822
|
+
/**
|
|
1823
|
+
* Maximum size of the evidence document in bytes (default: 262144)
|
|
1824
|
+
*/
|
|
1825
|
+
maxEvidenceSize?: number;
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* Time limit for each download in milliseconds, including redirects and the body (default: 30000)
|
|
1829
|
+
*/
|
|
1830
|
+
timeout?: number;
|
|
1831
|
+
|
|
1832
|
+
/**
|
|
1833
|
+
* undici Dispatcher used for the downloads
|
|
1834
|
+
*/
|
|
1835
|
+
dispatcher?: any;
|
|
1836
|
+
|
|
1682
1837
|
/**
|
|
1683
1838
|
* Custom VMC validation options passed to @postalsys/vmc
|
|
1684
1839
|
*/
|
|
@@ -1702,13 +1857,30 @@ export function bimi(opts: BIMIOptions): Promise<BIMIResult | false>;
|
|
|
1702
1857
|
*/
|
|
1703
1858
|
export function validateBimiVmc(bimiData: BIMIData | null, opts?: VMCValidationOptions): Promise<VMCValidationResult | false>;
|
|
1704
1859
|
|
|
1860
|
+
/**
|
|
1861
|
+
* SVG logo validation options
|
|
1862
|
+
*/
|
|
1863
|
+
export interface BIMISvgValidationOptions {
|
|
1864
|
+
/**
|
|
1865
|
+
* Apply the SVG Tiny PS profile exactly (default: false)
|
|
1866
|
+
*/
|
|
1867
|
+
strict?: boolean;
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* Markers of the accepted profile deviations are added to this array when `strict` is not set
|
|
1871
|
+
*/
|
|
1872
|
+
warnings?: string[];
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1705
1875
|
/**
|
|
1706
1876
|
* Validates BIMI SVG logo file
|
|
1707
1877
|
*
|
|
1708
1878
|
* @param logo - SVG logo file buffer
|
|
1879
|
+
* @param opts - validation options
|
|
1880
|
+
* @returns true
|
|
1709
1881
|
* @throws Error if validation fails
|
|
1710
1882
|
*/
|
|
1711
|
-
export function validateBimiSvg(logo: Buffer):
|
|
1883
|
+
export function validateBimiSvg(logo: Buffer | string, opts?: BIMISvgValidationOptions): true;
|
|
1712
1884
|
|
|
1713
1885
|
// ============================================================================
|
|
1714
1886
|
// MTA-STS
|
|
@@ -1752,6 +1924,16 @@ export interface MTASTSPolicy {
|
|
|
1752
1924
|
* Error encountered during policy fetch
|
|
1753
1925
|
*/
|
|
1754
1926
|
error?: Error;
|
|
1927
|
+
|
|
1928
|
+
/**
|
|
1929
|
+
* Policy ID from DNS that could not be fetched, set on a cached policy that is kept in use instead
|
|
1930
|
+
*/
|
|
1931
|
+
retryId?: string;
|
|
1932
|
+
|
|
1933
|
+
/**
|
|
1934
|
+
* ISO 8601 timestamp before which the policy for `retryId` is not fetched again (RFC 8461 3.3)
|
|
1935
|
+
*/
|
|
1936
|
+
retryAfter?: string;
|
|
1755
1937
|
}
|
|
1756
1938
|
|
|
1757
1939
|
/**
|
package/lib/arc/index.d.ts
CHANGED
|
@@ -44,6 +44,10 @@ export function verifyASChain(data: ARCData, opts: ARCOptions & { warnings?: str
|
|
|
44
44
|
*
|
|
45
45
|
* @param input - RFC822 formatted message or false for pre-calculated data
|
|
46
46
|
* @param data - Seal creation data
|
|
47
|
-
* @returns Seal headers, empty if the ARC-Message-Signature could not be signed,
|
|
47
|
+
* @returns Seal headers, empty if the ARC-Message-Signature could not be signed, any errors, and
|
|
48
|
+
* the instance (i=) of a created set
|
|
48
49
|
*/
|
|
49
|
-
export function createSeal(
|
|
50
|
+
export function createSeal(
|
|
51
|
+
input: MessageInput | false,
|
|
52
|
+
data: ARCCreateSealData
|
|
53
|
+
): Promise<{ headers: string[]; errors: ARCSealError[]; warnings: string[]; instance?: number }>;
|
package/lib/arc/index.js
CHANGED
|
@@ -14,7 +14,8 @@ const {
|
|
|
14
14
|
addWarning,
|
|
15
15
|
KEY_ERROR_COMMENTS,
|
|
16
16
|
splitAlgorithm,
|
|
17
|
-
createKeyCache
|
|
17
|
+
createKeyCache,
|
|
18
|
+
isHostNameDomain
|
|
18
19
|
} = require('../../lib/tools');
|
|
19
20
|
const { TIMESTAMP, BODY_LENGTH, MAX_ARC_INSTANCE, ARC_HEADER_KEYS } = require('../dkim/syntax');
|
|
20
21
|
const crypto = require('node:crypto');
|
|
@@ -472,6 +473,15 @@ const getARChain = (headers, opts) => {
|
|
|
472
473
|
checkTagList(arcInstance, 'arc-message-signature', strict, warn);
|
|
473
474
|
}
|
|
474
475
|
|
|
476
|
+
// A d= that is not a host name does not name the zone of its key record (RFC 6376
|
|
477
|
+
// section 3.5), so its key is never looked up and the chain fails in every mode
|
|
478
|
+
for (let headerKey of isNewest ? ['arc-seal', 'arc-message-signature'] : ['arc-seal']) {
|
|
479
|
+
let signingDomain = (arcInstance[headerKey].parsed?.d?.value || '').toString();
|
|
480
|
+
if (signingDomain && !isHostNameDomain(signingDomain)) {
|
|
481
|
+
throw createError(`i=${arcInstance.i} ${SHORT_NAMES[headerKey]} invalid d=`, 'invalid_arc_tags');
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
475
485
|
let cv = getCv(arcInstance);
|
|
476
486
|
|
|
477
487
|
if (i === 0 && cv !== 'none') {
|
|
@@ -605,8 +615,11 @@ const arc = async (data, opts) => {
|
|
|
605
615
|
// all failures are permanent in the scope of ARC
|
|
606
616
|
result.i = data?.lastEntry?.i || false;
|
|
607
617
|
status.result = 'fail';
|
|
608
|
-
//
|
|
609
|
-
|
|
618
|
+
// RFC 8617 section 5.1 step 2: a failed chain is sealed with cv=fail unless its newest
|
|
619
|
+
// seal already says cv=fail. The DKIM verifier reads the newest seal from the header
|
|
620
|
+
// fields (latestCv), so a chain that failed the structure checks and could not be
|
|
621
|
+
// parsed is sealed as well. Data from elsewhere only has the parsed chain
|
|
622
|
+
status.shouldSeal = data?.latestCv !== undefined ? data.latestCv !== 'fail' : ['pass', 'none'].includes(getCv(data?.lastEntry));
|
|
610
623
|
|
|
611
624
|
switch (err.code) {
|
|
612
625
|
case 'invalid_arc_seal':
|
|
@@ -699,8 +712,8 @@ const arc = async (data, opts) => {
|
|
|
699
712
|
|
|
700
713
|
const sealError = (seal, err) => ({
|
|
701
714
|
type: 'ARC',
|
|
702
|
-
selector: seal
|
|
703
|
-
signingDomain: seal
|
|
715
|
+
selector: seal.selector,
|
|
716
|
+
signingDomain: seal.signingDomain,
|
|
704
717
|
err
|
|
705
718
|
});
|
|
706
719
|
|
|
@@ -713,14 +726,17 @@ const isSet = value => typeof value !== 'undefined' && value !== null && value !
|
|
|
713
726
|
* @param {ReadableStream|Buffer|String|false} input The message, or false to use `data.headers`
|
|
714
727
|
* and `seal.bodyHash` instead
|
|
715
728
|
* @param {Object} data { headers, arc, seal, strict }
|
|
716
|
-
* @returns {Object} { headers, errors, warnings }. `headers` is empty when no set was
|
|
717
|
-
* and `errors` says why
|
|
729
|
+
* @returns {Object} { headers, errors, warnings, instance }. `headers` is empty when no set was
|
|
730
|
+
* created, and `errors` says why. `instance` is the i= of a created set
|
|
718
731
|
*/
|
|
719
732
|
const createSeal = async (input, data) => {
|
|
720
|
-
let { headers, arc
|
|
721
|
-
|
|
733
|
+
let { headers, arc } = data;
|
|
734
|
+
// a copy, the instance chosen below is stored on it and the caller's object, which may be
|
|
735
|
+
// reused for the next message, stays unchanged
|
|
736
|
+
let seal = Object.assign({}, data.seal);
|
|
737
|
+
let bodyHash = seal.bodyHash;
|
|
722
738
|
|
|
723
|
-
const strict = !!(data.strict || seal
|
|
739
|
+
const strict = !!(data.strict || seal.strict);
|
|
724
740
|
const warnings = [];
|
|
725
741
|
const warn = warning => addWarning(warnings, warning);
|
|
726
742
|
|
|
@@ -761,6 +777,19 @@ const createSeal = async (input, data) => {
|
|
|
761
777
|
}
|
|
762
778
|
}
|
|
763
779
|
|
|
780
|
+
// RFC 8617 section 4.1.1, the payload of the ARC-Authentication-Results header. A line break
|
|
781
|
+
// is only allowed as folding (CRLF followed by whitespace and more content), anything else
|
|
782
|
+
// would start a new header field or end the header block
|
|
783
|
+
let authResults = isSet(seal.authResults) ? String(seal.authResults) : '';
|
|
784
|
+
if (!/\S/.test(authResults)) {
|
|
785
|
+
return refuse('The authResults value for the ARC-Authentication-Results header is missing', 'EINVALIDAUTHRESULTS');
|
|
786
|
+
}
|
|
787
|
+
// a CR that does not start folding, or an LF without a CR. A search for the bad line break,
|
|
788
|
+
// a pattern that matches the whole value runs out of stack on a large one
|
|
789
|
+
if (/\r(?!\n[ \t]+[^ \t\r\n])|(?<!\r)\n/.test(authResults)) {
|
|
790
|
+
return refuse('The authResults value contains a line break that is not header folding', 'EINVALIDAUTHRESULTS');
|
|
791
|
+
}
|
|
792
|
+
|
|
764
793
|
// Step 1. Calculate ARC-Message-Signature
|
|
765
794
|
dkimSigner = new DkimSigner({
|
|
766
795
|
// headers and bodyHash are prepared values if we do not have the source message anymore
|
|
@@ -869,7 +898,7 @@ const createSeal = async (input, data) => {
|
|
|
869
898
|
return { headers: [], errors: dkimSigner.errors, warnings: collectWarnings() };
|
|
870
899
|
}
|
|
871
900
|
|
|
872
|
-
const
|
|
901
|
+
const authResultsHeader = `ARC-Authentication-Results: i=${seal.i}; ${authResults}`;
|
|
873
902
|
|
|
874
903
|
// the ARC-Seal uses the algorithm of the ARC-Message-Signature, which follows the key type
|
|
875
904
|
// when no algorithm was given
|
|
@@ -881,7 +910,7 @@ const createSeal = async (input, data) => {
|
|
|
881
910
|
arcSeal = await signAS(
|
|
882
911
|
arc?.chain || [],
|
|
883
912
|
{
|
|
884
|
-
'arc-authentication-results':
|
|
913
|
+
'arc-authentication-results': authResultsHeader,
|
|
885
914
|
'arc-message-signature': messageSignature
|
|
886
915
|
},
|
|
887
916
|
{
|
|
@@ -900,9 +929,10 @@ const createSeal = async (input, data) => {
|
|
|
900
929
|
}
|
|
901
930
|
|
|
902
931
|
return {
|
|
903
|
-
headers: [arcSeal, messageSignature,
|
|
932
|
+
headers: [arcSeal, messageSignature, authResultsHeader],
|
|
904
933
|
errors: dkimSigner.errors,
|
|
905
|
-
warnings: collectWarnings()
|
|
934
|
+
warnings: collectWarnings(),
|
|
935
|
+
instance: seal.i
|
|
906
936
|
};
|
|
907
937
|
};
|
|
908
938
|
|
|
@@ -911,4 +941,4 @@ const sealMessage = async (input, seal) => {
|
|
|
911
941
|
return headers.length ? Buffer.from(headers.join('\r\n') + '\r\n') : Buffer.from('');
|
|
912
942
|
};
|
|
913
943
|
|
|
914
|
-
module.exports = { getARChain, verifyASChain, arc, createSeal, sealMessage };
|
|
944
|
+
module.exports = { getARChain, getHighestArcInstance, verifyASChain, arc, createSeal, sealMessage };
|