mailauth 4.12.1 → 4.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +13 -0
- package/LICENSE.txt +1 -1
- package/README.md +16 -2
- package/cli.md +2 -1
- package/docs/arc.md +176 -0
- package/docs/bimi.md +272 -0
- package/docs/dkim.md +202 -0
- package/docs/dmarc.md +225 -0
- package/docs/mta-sts.md +239 -0
- package/docs/spf.md +196 -0
- package/lib/bimi/index.js +20 -0
- package/lib/dkim/dkim-verifier.js +17 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.13.0](https://github.com/postalsys/mailauth/compare/v4.12.1...v4.13.0) (2026-02-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **bimi:** add BIMI headers to VMC validation output ([77ce4e8](https://github.com/postalsys/mailauth/commit/77ce4e847d79b2acf319c3b79e865225e4c97040))
|
|
9
|
+
* **dkim:** add timestamp, expiration, and validity status to output ([2267eb7](https://github.com/postalsys/mailauth/commit/2267eb77fbcaf79a6c0fb59681815628186a3ae9))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* update Node.js requirement to >=20.18.1 ([3280a59](https://github.com/postalsys/mailauth/commit/3280a597430cf47b640b1cc31e661ab18becf145)), closes [#109](https://github.com/postalsys/mailauth/issues/109)
|
|
15
|
+
|
|
3
16
|
## [4.12.1](https://github.com/postalsys/mailauth/compare/v4.12.0...v4.12.1) (2026-02-01)
|
|
4
17
|
|
|
5
18
|
|
package/LICENSE.txt
CHANGED
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
- **BIMI** resolving and **VMC** validation
|
|
16
16
|
- **MTA-STS** helper functions
|
|
17
17
|
|
|
18
|
-
mailauth is a pure JavaScript implementation, requiring no external applications or compilation. It runs on any server or device with Node.js version
|
|
18
|
+
mailauth is a pure JavaScript implementation, requiring no external applications or compilation. It runs on any server or device with Node.js version 20.18.1 or later.
|
|
19
19
|
|
|
20
20
|
> [!NOTE]
|
|
21
21
|
> mailauth is used by [EmailEngine](https://emailengine.app/) for validating email authentication settings. See the [Email Authentication Testing documentation](https://learn.emailengine.app/docs/advanced/email-authentication-testing) for details.
|
|
@@ -163,6 +163,8 @@ const signResult = await dkimSign(message, options);
|
|
|
163
163
|
- **canonicalization** (`string`): Canonicalization method. Defaults to `'relaxed/relaxed'`.
|
|
164
164
|
- **algorithm** (`string`): Signing and hashing algorithm. Defaults to `'rsa-sha256'`.
|
|
165
165
|
- **signTime** (`Date`): Signing time. Defaults to current time.
|
|
166
|
+
- **expires** (`Date`): Signature expiration time (`x=` tag). Optional.
|
|
167
|
+
- **headerList** (`Array`): List of header field names to sign. Optional; uses default set if not specified.
|
|
166
168
|
- **signatureData** (`Array`): Array of signature objects. Each object may contain:
|
|
167
169
|
- **signingDomain** (`string`): DKIM key domain name.
|
|
168
170
|
- **selector** (`string`): DKIM key selector.
|
|
@@ -250,6 +252,8 @@ const result = await dkimVerify(message);
|
|
|
250
252
|
// Returns an object containing verification results
|
|
251
253
|
```
|
|
252
254
|
|
|
255
|
+
See [DKIM Result Reference](docs/dkim.md) for details on the result object structure.
|
|
256
|
+
|
|
253
257
|
##### Example
|
|
254
258
|
|
|
255
259
|
```javascript
|
|
@@ -287,6 +291,8 @@ const result = await spf(options);
|
|
|
287
291
|
// Returns an object containing SPF verification results
|
|
288
292
|
```
|
|
289
293
|
|
|
294
|
+
See [SPF Result Reference](docs/spf.md) for details on the result object structure.
|
|
295
|
+
|
|
290
296
|
##### Parameters
|
|
291
297
|
|
|
292
298
|
- **options**:
|
|
@@ -338,6 +344,8 @@ const { arc } = await authenticate(message, {
|
|
|
338
344
|
console.log(arc);
|
|
339
345
|
```
|
|
340
346
|
|
|
347
|
+
See [ARC Result Reference](docs/arc.md) for details on the result object structure.
|
|
348
|
+
|
|
341
349
|
**Sample Output:**
|
|
342
350
|
|
|
343
351
|
```json
|
|
@@ -419,6 +427,8 @@ process.stdout.write(message);
|
|
|
419
427
|
|
|
420
428
|
DMARC is verified during the authentication process. Although the `dmarc` handler is exported, it requires input from previous steps like SPF and DKIM.
|
|
421
429
|
|
|
430
|
+
See [DMARC Result Reference](docs/dmarc.md) for details on the result object structure.
|
|
431
|
+
|
|
422
432
|
#### DMARC Helpers
|
|
423
433
|
|
|
424
434
|
##### `getDmarcRecord(domain [, resolver])`
|
|
@@ -467,6 +477,8 @@ console.log(dmarcRecord);
|
|
|
467
477
|
|
|
468
478
|
Brand Indicators for Message Identification (BIMI) support is based on [draft-blank-ietf-bimi-02](https://tools.ietf.org/html/draft-blank-ietf-bimi-02). BIMI information is resolved during the authentication step, provided the message passes DMARC validation with a policy other than "none".
|
|
469
479
|
|
|
480
|
+
See [BIMI Result Reference](docs/bimi.md) for details on the result object structure.
|
|
481
|
+
|
|
470
482
|
#### Example
|
|
471
483
|
|
|
472
484
|
```javascript
|
|
@@ -518,6 +530,8 @@ const { policy, status } = await getPolicy(domain [, knownPolicy]);
|
|
|
518
530
|
// Returns an object with the policy and status
|
|
519
531
|
```
|
|
520
532
|
|
|
533
|
+
See [MTA-STS Result Reference](docs/mta-sts.md) for details on the result object structure.
|
|
534
|
+
|
|
521
535
|
##### Parameters
|
|
522
536
|
|
|
523
537
|
- **domain** (`string`): The domain to retrieve the policy for.
|
|
@@ -604,6 +618,6 @@ Based on ValiMail's [arc_test_suite](https://github.com/ValiMail/arc_test_suite)
|
|
|
604
618
|
|
|
605
619
|
## License
|
|
606
620
|
|
|
607
|
-
© 2020-
|
|
621
|
+
© 2020-2026 Postal Systems OÜ
|
|
608
622
|
|
|
609
623
|
Licensed under the [MIT License](LICENSE).
|
package/cli.md
CHANGED
|
@@ -268,6 +268,7 @@ mailauth vmc [options]
|
|
|
268
268
|
- `--authority <url>`, `-a <url>`: URL of the VMC resource.
|
|
269
269
|
- `--authorityPath <path>`, `-p <path>`: Path to a local VMC file, used to avoid network requests.
|
|
270
270
|
- `--domain <domain>`, `-d <domain>`: Sender domain to validate against the certificate.
|
|
271
|
+
- `--date <timestamp>`, `-t <timestamp>`: ISO-formatted timestamp for certificate expiration checks. Defaults to the current time.
|
|
271
272
|
|
|
272
273
|
#### Example
|
|
273
274
|
|
|
@@ -421,6 +422,6 @@ When this option is used, mailauth will not perform actual DNS queries but will
|
|
|
421
422
|
|
|
422
423
|
## License
|
|
423
424
|
|
|
424
|
-
© 2020-
|
|
425
|
+
© 2020-2026 Postal Systems OÜ
|
|
425
426
|
|
|
426
427
|
Licensed under the [MIT License](LICENSE).
|
package/docs/arc.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# ARC Validation Result Reference
|
|
2
|
+
|
|
3
|
+
This document describes the result object returned by ARC (Authenticated Received Chain) validation and sealing.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
ARC validation is performed automatically during the authentication step. ARC allows intermediate mail handlers to sign messages, preserving authentication results across forwarding.
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const { authenticate } = require('mailauth');
|
|
11
|
+
|
|
12
|
+
const { arc } = await authenticate(message, {
|
|
13
|
+
trustReceived: true
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Validation Result Object Fields
|
|
18
|
+
|
|
19
|
+
| Field | Type | Presence | Description |
|
|
20
|
+
|-------|------|----------|-------------|
|
|
21
|
+
| `status` | `object` | Always | Validation status object (see below) |
|
|
22
|
+
| `chain` | `array` | Non-enumerable | Array of ARC chain entries (hidden from JSON serialization) |
|
|
23
|
+
| `i` | `number\|false` | Always | Last instance number in chain, or `false` if no chain |
|
|
24
|
+
| `signature` | `object` | Chain exists | Verification result for last ARC-Message-Signature |
|
|
25
|
+
| `authenticationResults` | `object` | Chain exists | Parsed last ARC-Authentication-Results |
|
|
26
|
+
| `info` | `string` | Result not "none" | Formatted Authentication-Results header value |
|
|
27
|
+
|
|
28
|
+
## status Object
|
|
29
|
+
|
|
30
|
+
| Field | Type | Presence | Description |
|
|
31
|
+
|-------|------|----------|-------------|
|
|
32
|
+
| `result` | `string` | Always | ARC result code (see below) |
|
|
33
|
+
| `comment` | `string` | On pass/fail | Description or error message |
|
|
34
|
+
| `shouldSeal` | `boolean` | On fail | Whether to continue sealing despite failure |
|
|
35
|
+
| `policy` | `object` | Policy issue | Policy violation details (e.g., `{"dkim-rules": "weak-key"}`) |
|
|
36
|
+
|
|
37
|
+
## authenticationResults Object
|
|
38
|
+
|
|
39
|
+
When ARC validation passes, the `authenticationResults` object contains parsed results from the last ARC-Authentication-Results header:
|
|
40
|
+
|
|
41
|
+
| Field | Type | Description |
|
|
42
|
+
|-------|------|-------------|
|
|
43
|
+
| `mta` | `string` | Hostname of the MTA that added this ARC set |
|
|
44
|
+
| `arc` | `object` | ARC result (`{result: "pass\|fail\|none", ...}`) |
|
|
45
|
+
| `spf` | `object` | SPF result (`{result: "pass\|fail\|...", ...}`) |
|
|
46
|
+
| `dmarc` | `object` | DMARC result (`{result: "pass\|fail\|none", header: {...}}`) |
|
|
47
|
+
| `dkim` | `array` | Array of DKIM results |
|
|
48
|
+
|
|
49
|
+
## Result Values
|
|
50
|
+
|
|
51
|
+
| Result | Description |
|
|
52
|
+
|--------|-------------|
|
|
53
|
+
| `none` | No ARC chain present in message |
|
|
54
|
+
| `pass` | ARC chain validated successfully |
|
|
55
|
+
| `fail` | ARC chain validation failed |
|
|
56
|
+
|
|
57
|
+
## Comment Values (on failure)
|
|
58
|
+
|
|
59
|
+
| Comment Pattern | Description |
|
|
60
|
+
|-----------------|-------------|
|
|
61
|
+
| `"i={n} seal signature validation failed"` | ARC-Seal cryptographic verification failed |
|
|
62
|
+
| `"i={n} no valid signature"` | ARC-Message-Signature verification failed |
|
|
63
|
+
| `"i={n} multiple {header} values"` | Duplicate ARC headers for same instance |
|
|
64
|
+
| `"chain-length={n}"` | Chain exceeds 50 instances |
|
|
65
|
+
| `"i={n} expected={m}"` | Missing or out-of-order instance numbers |
|
|
66
|
+
| `"i={n} no {header} set"` | Missing required ARC header |
|
|
67
|
+
| `"i=1 cv={value}"` | First instance must have `cv=none` |
|
|
68
|
+
| `"i={n} cv={value}"` | Non-first instance must have `cv=pass` |
|
|
69
|
+
| `"i={n} invalid as c"` | Invalid canonicalization for ARC-Seal |
|
|
70
|
+
| `"i={n} invalid ams h"` | ARC-Message-Signature signed arc-seal (forbidden) |
|
|
71
|
+
| `"no key for {domain}"` | DNS key not found |
|
|
72
|
+
| `"unknown key version for {domain}"` | Unsupported key version |
|
|
73
|
+
| `"unknown key type for {domain}"` | Unsupported key type |
|
|
74
|
+
| `"invalid public key for {domain}"` | Malformed public key |
|
|
75
|
+
| `"weak key for {domain}"` | RSA key too short |
|
|
76
|
+
|
|
77
|
+
## Seal Result Object
|
|
78
|
+
|
|
79
|
+
When sealing a message with `sealMessage()`, the result contains:
|
|
80
|
+
|
|
81
|
+
| Field | Type | Description |
|
|
82
|
+
|-------|------|-------------|
|
|
83
|
+
| `headers` | `string[]` | Array of ARC headers to prepend: `[ARC-Seal, ARC-Message-Signature, ARC-Authentication-Results]` |
|
|
84
|
+
|
|
85
|
+
## Example Output
|
|
86
|
+
|
|
87
|
+
### ARC Pass
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"status": {
|
|
92
|
+
"result": "pass",
|
|
93
|
+
"comment": "i=2 spf=pass dkim=pass dkdomain=example.com dmarc=pass fromdomain=example.com"
|
|
94
|
+
},
|
|
95
|
+
"i": 2,
|
|
96
|
+
"signature": {
|
|
97
|
+
"id": "abc123...",
|
|
98
|
+
"signingDomain": "forwarder.example.net",
|
|
99
|
+
"selector": "arc",
|
|
100
|
+
"status": {
|
|
101
|
+
"result": "pass"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"authenticationResults": {
|
|
105
|
+
"mta": "mx.forwarder.example.net",
|
|
106
|
+
"spf": {
|
|
107
|
+
"result": "pass",
|
|
108
|
+
"smtp": {
|
|
109
|
+
"mailfrom": "user@example.com"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"dkim": [
|
|
113
|
+
{
|
|
114
|
+
"result": "pass",
|
|
115
|
+
"header": {
|
|
116
|
+
"i": "@example.com",
|
|
117
|
+
"s": "selector1"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"dmarc": {
|
|
122
|
+
"result": "pass",
|
|
123
|
+
"header": {
|
|
124
|
+
"from": "example.com"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"info": "arc=pass (i=2 spf=pass dkim=pass dkdomain=example.com dmarc=pass fromdomain=example.com)"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### ARC Fail
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"status": {
|
|
137
|
+
"result": "fail",
|
|
138
|
+
"comment": "i=2 seal signature validation failed",
|
|
139
|
+
"shouldSeal": true
|
|
140
|
+
},
|
|
141
|
+
"i": 2,
|
|
142
|
+
"info": "arc=fail (i=2 seal signature validation failed)"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### ARC None
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"status": {
|
|
151
|
+
"result": "none"
|
|
152
|
+
},
|
|
153
|
+
"i": 0
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Seal Headers Output
|
|
158
|
+
|
|
159
|
+
When using `sealMessage()`:
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
const { sealMessage } = require('mailauth');
|
|
163
|
+
|
|
164
|
+
const sealHeaders = await sealMessage(message, {
|
|
165
|
+
signingDomain: 'example.com',
|
|
166
|
+
selector: 'arc',
|
|
167
|
+
privateKey: privateKey,
|
|
168
|
+
authResults: 'mx.example.com; spf=pass; dkim=pass',
|
|
169
|
+
cv: 'pass'
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// sealHeaders is a Buffer containing:
|
|
173
|
+
// ARC-Seal: i=1; a=rsa-sha256; cv=pass; d=example.com; s=arc; ...
|
|
174
|
+
// ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; ...
|
|
175
|
+
// ARC-Authentication-Results: i=1; mx.example.com; spf=pass; dkim=pass
|
|
176
|
+
```
|
package/docs/bimi.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# BIMI Result Reference
|
|
2
|
+
|
|
3
|
+
This document describes the result object returned by BIMI (Brand Indicators for Message Identification) resolution.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
BIMI allows organizations to display brand logos in email clients. BIMI information is resolved during the authentication step, provided the message passes DMARC validation with a policy other than "none".
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const { authenticate } = require('mailauth');
|
|
11
|
+
|
|
12
|
+
const { bimi } = await authenticate(message, {
|
|
13
|
+
ip: '192.0.2.1',
|
|
14
|
+
helo: 'mail.example.com',
|
|
15
|
+
sender: 'user@example.com'
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Result Object Fields
|
|
20
|
+
|
|
21
|
+
| Field | Type | Presence | Description |
|
|
22
|
+
|-------|------|----------|-------------|
|
|
23
|
+
| `status` | `object` | Always | Resolution status object (see below) |
|
|
24
|
+
| `location` | `string` | Found | HTTPS URL for the logo SVG file (from `l=` tag) |
|
|
25
|
+
| `authority` | `string` | Found | HTTPS URL for the VMC/CMC certificate (from `a=` tag) |
|
|
26
|
+
| `preference` | `string` | When `p=` present | Logo preference value from DNS record |
|
|
27
|
+
| `rr` | `string` | Found | Raw BIMI DNS TXT record |
|
|
28
|
+
| `info` | `string` | Always | Formatted Authentication-Results header value |
|
|
29
|
+
|
|
30
|
+
## status Object
|
|
31
|
+
|
|
32
|
+
| Field | Type | Presence | Description |
|
|
33
|
+
|-------|------|----------|-------------|
|
|
34
|
+
| `result` | `string` | Always | BIMI result code (see below) |
|
|
35
|
+
| `comment` | `string` | On skip/fail | Reason for skip or failure |
|
|
36
|
+
| `header` | `object` | Always | Header information |
|
|
37
|
+
| `policy` | `object` | VMC found | Authority policy details |
|
|
38
|
+
|
|
39
|
+
### status.header Object
|
|
40
|
+
|
|
41
|
+
| Field | Type | Presence | Description |
|
|
42
|
+
|-------|------|----------|-------------|
|
|
43
|
+
| `selector` | `string` | Record found | BIMI selector used (e.g., `"default"`) |
|
|
44
|
+
| `d` | `string` | Record found | Domain where BIMI record was found |
|
|
45
|
+
|
|
46
|
+
### status.policy Object
|
|
47
|
+
|
|
48
|
+
| Field | Type | Description |
|
|
49
|
+
|-------|------|-------------|
|
|
50
|
+
| `authority` | `string` | VMC validation status (`"none"` before validation) |
|
|
51
|
+
| `authority-uri` | `string` | URL of the authority evidence document |
|
|
52
|
+
|
|
53
|
+
## Result Values
|
|
54
|
+
|
|
55
|
+
| Result | Description |
|
|
56
|
+
|--------|-------------|
|
|
57
|
+
| `pass` | BIMI record found and valid |
|
|
58
|
+
| `skipped` | BIMI lookup skipped (see skip reasons below) |
|
|
59
|
+
| `fail` | BIMI record found but invalid |
|
|
60
|
+
| `none` | No BIMI record found |
|
|
61
|
+
| `temperr` | Temporary error during DNS lookup |
|
|
62
|
+
|
|
63
|
+
## Skip Reasons
|
|
64
|
+
|
|
65
|
+
The `status.comment` field explains why BIMI was skipped:
|
|
66
|
+
|
|
67
|
+
| Comment | Description |
|
|
68
|
+
|---------|-------------|
|
|
69
|
+
| `"DMARC not enabled"` | DMARC result was `none` |
|
|
70
|
+
| `"message failed DMARC"` | DMARC result was not `pass` |
|
|
71
|
+
| `"too lax DMARC policy"` | DMARC policy is `none` or `quarantine` with `pct < 100` |
|
|
72
|
+
| `"Aligned DKIM signature required"` | `bimiWithAlignedDkim` option set but no aligned DKIM |
|
|
73
|
+
| `"undersized DKIM signature"` | DKIM signature has unsigned body bytes (due to `l=` tag) |
|
|
74
|
+
| `"could not determine domain"` | Unable to extract domain from headers |
|
|
75
|
+
|
|
76
|
+
## Fail Reasons
|
|
77
|
+
|
|
78
|
+
| Comment | Description |
|
|
79
|
+
|---------|-------------|
|
|
80
|
+
| `"multiple BIMI-Selector headers"` | Message has more than one BIMI-Selector header |
|
|
81
|
+
| `"missing bimi version in selector header"` | BIMI-Selector header missing `v=BIMI1` |
|
|
82
|
+
| `"missing bimi version in dns record"` | DNS record missing `v=BIMI1` |
|
|
83
|
+
| `"missing location value in dns record"` | Record has neither `l=` nor `a=` tag |
|
|
84
|
+
| `"invalid location value in dns record"` | `l=` value is not a valid HTTPS URL |
|
|
85
|
+
| `"invalid authority value in dns record"` | `a=` value is not a valid HTTPS URL |
|
|
86
|
+
| `"failed to resolve {domain}"` | DNS lookup error |
|
|
87
|
+
| `"invalid BIMI response for {domain}"` | DNS response format invalid |
|
|
88
|
+
|
|
89
|
+
## VMC Validation Result
|
|
90
|
+
|
|
91
|
+
When using `validateVMC()` to validate the authority evidence document:
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
const { bimi, validateVMC } = require('mailauth/lib/bimi');
|
|
95
|
+
|
|
96
|
+
const bimiResult = await bimi(data);
|
|
97
|
+
const vmcResult = await validateVMC(bimiResult, options);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### VMC Result Object
|
|
101
|
+
|
|
102
|
+
| Field | Type | Description |
|
|
103
|
+
|-------|------|-------------|
|
|
104
|
+
| `location` | `object` | Logo file fetch result |
|
|
105
|
+
| `authority` | `object` | VMC/CMC fetch and validation result |
|
|
106
|
+
| `headers` | `object` | Ready-to-use email headers (only on validation success) |
|
|
107
|
+
|
|
108
|
+
### location Object
|
|
109
|
+
|
|
110
|
+
| Field | Type | Description |
|
|
111
|
+
|-------|------|-------------|
|
|
112
|
+
| `url` | `string` | Logo URL |
|
|
113
|
+
| `success` | `boolean` | Whether fetch succeeded |
|
|
114
|
+
| `logoFile` | `string` | Base64-encoded logo SVG (on success) |
|
|
115
|
+
| `error` | `object` | Error details (on failure) |
|
|
116
|
+
| `hashAlgo` | `string` | Hash algorithm used for verification |
|
|
117
|
+
| `hashValue` | `string` | Calculated hash of the logo file |
|
|
118
|
+
|
|
119
|
+
### authority Object
|
|
120
|
+
|
|
121
|
+
| Field | Type | Description |
|
|
122
|
+
|-------|------|-------------|
|
|
123
|
+
| `url` | `string` | VMC/CMC URL |
|
|
124
|
+
| `success` | `boolean` | Whether fetch and validation succeeded |
|
|
125
|
+
| `vmc` | `object` | Parsed VMC data (on success) |
|
|
126
|
+
| `domainVerified` | `boolean` | Whether domain matches certificate |
|
|
127
|
+
| `hashMatch` | `boolean` | Whether logo hash matches certificate |
|
|
128
|
+
| `error` | `object` | Error details (on failure) |
|
|
129
|
+
|
|
130
|
+
### headers Object
|
|
131
|
+
|
|
132
|
+
Present only when VMC validation succeeds. Contains ready-to-use email headers.
|
|
133
|
+
|
|
134
|
+
| Field | Type | Presence | Description |
|
|
135
|
+
|-------|------|----------|-------------|
|
|
136
|
+
| `indicator` | `string` | Always | BIMI-Indicator header with base64-encoded SVG logo |
|
|
137
|
+
| `location` | `string` | Always | BIMI-Location header with logo URL |
|
|
138
|
+
| `preference` | `string` | `p=` tag present | BIMI-Logo-Preference header |
|
|
139
|
+
|
|
140
|
+
These headers should be added to messages after successful BIMI validation. The MTA should:
|
|
141
|
+
1. Remove any existing BIMI-* headers from incoming messages
|
|
142
|
+
2. Add these headers after successful validation
|
|
143
|
+
|
|
144
|
+
### VMC Error Codes
|
|
145
|
+
|
|
146
|
+
| Code | Description |
|
|
147
|
+
|------|-------------|
|
|
148
|
+
| `HTTP_REQUEST_FAILED` | HTTP request failed |
|
|
149
|
+
| `MISSING_VMC_LOGO` | VMC does not contain a logo file |
|
|
150
|
+
| `INVALID_MEDIATYPE` | Logo media type is not `image/svg+xml` |
|
|
151
|
+
| `INVALID_LOGO_HASH` | Logo hash does not match certificate |
|
|
152
|
+
| `SVG_VALIDATION_FAILED` | SVG file failed validation |
|
|
153
|
+
| `VMC_DOMAIN_MISMATCH` | Domain not found in certificate SAN |
|
|
154
|
+
|
|
155
|
+
## Example Output
|
|
156
|
+
|
|
157
|
+
### BIMI Pass
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"status": {
|
|
162
|
+
"result": "pass",
|
|
163
|
+
"header": {
|
|
164
|
+
"selector": "default",
|
|
165
|
+
"d": "example.com"
|
|
166
|
+
},
|
|
167
|
+
"policy": {
|
|
168
|
+
"authority": "none",
|
|
169
|
+
"authority-uri": "https://example.com/bimi/vmc.pem"
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"location": "https://example.com/bimi/logo.svg",
|
|
173
|
+
"authority": "https://example.com/bimi/vmc.pem",
|
|
174
|
+
"preference": "self",
|
|
175
|
+
"rr": "v=BIMI1; l=https://example.com/bimi/logo.svg; a=https://example.com/bimi/vmc.pem; p=self",
|
|
176
|
+
"info": "bimi=pass header.selector=default header.d=example.com policy.authority=none policy.authority-uri=https://example.com/bimi/vmc.pem"
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### BIMI Skipped (DMARC Failed)
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"status": {
|
|
185
|
+
"result": "skipped",
|
|
186
|
+
"comment": "message failed DMARC",
|
|
187
|
+
"header": {}
|
|
188
|
+
},
|
|
189
|
+
"info": "bimi=skipped (message failed DMARC)"
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### BIMI Skipped (Policy Too Lax)
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"status": {
|
|
198
|
+
"result": "skipped",
|
|
199
|
+
"comment": "too lax DMARC policy",
|
|
200
|
+
"header": {}
|
|
201
|
+
},
|
|
202
|
+
"info": "bimi=skipped (too lax DMARC policy)"
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### BIMI None (No Record)
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"status": {
|
|
211
|
+
"result": "none",
|
|
212
|
+
"header": {
|
|
213
|
+
"selector": "default",
|
|
214
|
+
"d": "example.com"
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
"info": "bimi=none header.selector=default header.d=example.com"
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### BIMI Fail (Invalid Record)
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"status": {
|
|
226
|
+
"result": "fail",
|
|
227
|
+
"comment": "missing location value in dns record",
|
|
228
|
+
"header": {
|
|
229
|
+
"selector": "default",
|
|
230
|
+
"d": "example.com"
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
"rr": "v=BIMI1;",
|
|
234
|
+
"info": "bimi=fail (missing location value in dns record) header.selector=default header.d=example.com"
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### VMC Validation Result
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"location": {
|
|
243
|
+
"url": "https://example.com/bimi/logo.svg",
|
|
244
|
+
"success": true,
|
|
245
|
+
"logoFile": "PHN2ZyB4bWxucz0i...",
|
|
246
|
+
"hashAlgo": "sha256",
|
|
247
|
+
"hashValue": "abc123..."
|
|
248
|
+
},
|
|
249
|
+
"authority": {
|
|
250
|
+
"url": "https://example.com/bimi/vmc.pem",
|
|
251
|
+
"success": true,
|
|
252
|
+
"domainVerified": true,
|
|
253
|
+
"hashMatch": true,
|
|
254
|
+
"vmc": {
|
|
255
|
+
"type": "VMC",
|
|
256
|
+
"mediaType": "image/svg+xml",
|
|
257
|
+
"logoFile": "PHN2ZyB4bWxucz0i...",
|
|
258
|
+
"hashAlgo": "sha256",
|
|
259
|
+
"hashValue": "abc123...",
|
|
260
|
+
"validHash": true,
|
|
261
|
+
"certificate": {
|
|
262
|
+
"subjectAltName": ["example.com", "*.example.com"]
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
"headers": {
|
|
267
|
+
"indicator": "BIMI-Indicator: PHN2ZyB4bWxucz0i...",
|
|
268
|
+
"location": "BIMI-Location: v=BIMI1; l=https://example.com/bimi/logo.svg",
|
|
269
|
+
"preference": "BIMI-Logo-Preference: self"
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|