@blamejs/core 0.9.46 → 0.10.1
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/CHANGELOG.md +951 -893
- package/index.js +30 -0
- package/lib/_test/crypto-fixtures.js +67 -0
- package/lib/agent-event-bus.js +52 -6
- package/lib/agent-idempotency.js +169 -16
- package/lib/agent-orchestrator.js +263 -9
- package/lib/agent-posture-chain.js +163 -5
- package/lib/agent-saga.js +146 -16
- package/lib/agent-snapshot.js +349 -19
- package/lib/agent-stream.js +34 -2
- package/lib/agent-tenant.js +179 -23
- package/lib/agent-trace.js +84 -21
- package/lib/auth/aal.js +8 -1
- package/lib/auth/ciba.js +6 -1
- package/lib/auth/dpop.js +7 -2
- package/lib/auth/fal.js +17 -8
- package/lib/auth/jwt-external.js +128 -4
- package/lib/auth/oauth.js +232 -10
- package/lib/auth/oid4vci.js +67 -7
- package/lib/auth/openid-federation.js +71 -25
- package/lib/auth/passkey.js +140 -6
- package/lib/auth/sd-jwt-vc.js +67 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/compliance.js +176 -8
- package/lib/crypto-field.js +114 -14
- package/lib/crypto.js +216 -20
- package/lib/db.js +1 -0
- package/lib/guard-imap-command.js +335 -0
- package/lib/guard-jmap.js +321 -0
- package/lib/guard-managesieve-command.js +566 -0
- package/lib/guard-pop3-command.js +317 -0
- package/lib/guard-smtp-command.js +58 -3
- package/lib/mail-agent.js +20 -7
- package/lib/mail-arc-sign.js +12 -8
- package/lib/mail-auth.js +323 -34
- package/lib/mail-crypto-pgp.js +934 -0
- package/lib/mail-crypto-smime.js +340 -0
- package/lib/mail-crypto.js +108 -0
- package/lib/mail-dav.js +1224 -0
- package/lib/mail-deploy.js +492 -0
- package/lib/mail-dkim.js +431 -26
- package/lib/mail-journal.js +435 -0
- package/lib/mail-scan.js +502 -0
- package/lib/mail-server-imap.js +1102 -0
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +164 -34
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +269 -0
- package/lib/mail-server-submission.js +1032 -0
- package/lib/mail-server-tls.js +445 -0
- package/lib/mail-sieve.js +557 -0
- package/lib/mail-spam-score.js +284 -0
- package/lib/mail.js +99 -0
- package/lib/metrics.js +130 -10
- package/lib/middleware/dpop.js +58 -3
- package/lib/middleware/idempotency-key.js +255 -42
- package/lib/middleware/protected-resource-metadata.js +114 -2
- package/lib/network-dns-resolver.js +33 -0
- package/lib/network-tls.js +46 -0
- package/lib/outbox.js +62 -12
- package/lib/pqc-agent.js +13 -5
- package/lib/retry.js +23 -9
- package/lib/router.js +23 -1
- package/lib/safe-ical.js +634 -0
- package/lib/safe-icap.js +502 -0
- package/lib/safe-mime.js +15 -0
- package/lib/safe-sieve.js +684 -0
- package/lib/safe-smtp.js +57 -0
- package/lib/safe-url.js +37 -0
- package/lib/safe-vcard.js +473 -0
- package/lib/self-update-standalone-verifier.js +32 -3
- package/lib/self-update.js +168 -17
- package/lib/vendor/MANIFEST.json +161 -156
- package/lib/vendor-data.js +127 -9
- package/lib/vex.js +324 -59
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module b.mail.deploy
|
|
5
|
+
* @nav Mail
|
|
6
|
+
* @title Mail deployment helpers
|
|
7
|
+
* @order 250
|
|
8
|
+
* @since 0.9.56
|
|
9
|
+
*
|
|
10
|
+
* @intro
|
|
11
|
+
* Operator-deployment helpers for standing up a blamejs mail
|
|
12
|
+
* server. Generates the policy text + DNS records + client
|
|
13
|
+
* auto-discovery XML every deployment needs alongside the wire-
|
|
14
|
+
* protocol primitives. Pairs with existing verifiers
|
|
15
|
+
* (`b.network.smtp.policy` carries the inbound MTA-STS / TLS-RPT
|
|
16
|
+
* evaluation logic shipped pre-v0.9.46; `b.mail.bimi` carries the
|
|
17
|
+
* inbound BIMI trust-anchor verifier) so the publish-side helpers
|
|
18
|
+
* stay thin and the operator runs one vocabulary across both sides.
|
|
19
|
+
*
|
|
20
|
+
* Surface:
|
|
21
|
+
* - `b.mail.deploy.mtaStsPublish(opts)` — RFC 8461 §3.2
|
|
22
|
+
* `/.well-known/mta-sts.txt` policy text + DNS TXT record advice
|
|
23
|
+
* + DNS record-name advice. Pairs with the inbound MTA-STS
|
|
24
|
+
* verifier on the receiving side.
|
|
25
|
+
* - `b.mail.deploy.danePublish(opts)` — RFC 7672 + RFC 6698 TLSA
|
|
26
|
+
* record generator. Computes SHA-256 SubjectPublicKeyInfo hash
|
|
27
|
+
* from an operator-supplied PEM cert, returns the TLSA record
|
|
28
|
+
* string for the operator's DNS zone.
|
|
29
|
+
* - `b.mail.deploy.autoConfigXml(opts)` — Thunderbird's
|
|
30
|
+
* `autoconfig.example.com/mail/config-v1.1.xml` shape. RFC-less
|
|
31
|
+
* (Mozilla convention) but documented at
|
|
32
|
+
* https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
|
|
33
|
+
* - `b.mail.deploy.autoDiscoverXml(opts)` — Outlook's
|
|
34
|
+
* `autodiscover.example.com/autodiscover/autodiscover.xml`
|
|
35
|
+
* response shape. MS-OXDSCLI Section 5 + MS-OXDISCO.
|
|
36
|
+
*
|
|
37
|
+
* The XML generators emit single-string output the operator wires
|
|
38
|
+
* into `b.staticServe` (mta-sts.txt + autoconfig.xml) or a route
|
|
39
|
+
* handler (autodiscover, which is request-conditional). No new
|
|
40
|
+
* network surface — these are pure deterministic functions.
|
|
41
|
+
*
|
|
42
|
+
* @card
|
|
43
|
+
* Operator-deployment helpers: MTA-STS / DANE / autoconfig /
|
|
44
|
+
* autodiscover text generators. Pair with the existing inbound
|
|
45
|
+
* verifiers to complete the publish ↔ verify cycle.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
var nodeCrypto = require("node:crypto");
|
|
49
|
+
var validateOpts = require("./validate-opts");
|
|
50
|
+
var numericBounds = require("./numeric-bounds");
|
|
51
|
+
var { defineClass } = require("./framework-error");
|
|
52
|
+
|
|
53
|
+
var MailDeployError = defineClass("MailDeployError", { alwaysPermanent: true });
|
|
54
|
+
|
|
55
|
+
// RFC 8461 §3.2 MTA-STS policy field allowlist. Field values typed +
|
|
56
|
+
// bounded — operator supplies them; we never echo arbitrary bytes
|
|
57
|
+
// into a DNS-resolvable resource.
|
|
58
|
+
var STS_MODES = Object.freeze({ enforce: 1, testing: 1, none: 1 });
|
|
59
|
+
|
|
60
|
+
function _domainOk(d) {
|
|
61
|
+
if (typeof d !== "string" || d.length === 0 || d.length > 253) return false; // allow:raw-byte-literal — RFC 1035 §2.3.4
|
|
62
|
+
// Bounded LDH check; we don't pull in b.guardDomain here because
|
|
63
|
+
// the helper is text-generation and the operator owns the value.
|
|
64
|
+
// Refuse C0 (covers CR / LF / NUL), DEL, and `"` outright —
|
|
65
|
+
// header-injection class + XML-attribute-injection class.
|
|
66
|
+
for (var i = 0; i < d.length; i++) {
|
|
67
|
+
var c = d.charCodeAt(i);
|
|
68
|
+
if (c < 0x20 || c === 0x7F || c === 0x22) return false; // allow:raw-byte-literal — refuse C0 / DEL / "
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _xmlEscape(s) {
|
|
74
|
+
return String(s)
|
|
75
|
+
.replace(/&/g, "&")
|
|
76
|
+
.replace(/</g, "<")
|
|
77
|
+
.replace(/>/g, ">")
|
|
78
|
+
.replace(/"/g, """)
|
|
79
|
+
.replace(/'/g, "'");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @primitive b.mail.deploy.mtaStsPublish
|
|
84
|
+
* @signature b.mail.deploy.mtaStsPublish(opts)
|
|
85
|
+
* @since 0.9.56
|
|
86
|
+
* @status stable
|
|
87
|
+
* @related b.mail.deploy.danePublish
|
|
88
|
+
*
|
|
89
|
+
* Generate the MTA-STS policy file ([RFC 8461 §3.2](https://www.rfc-editor.org/rfc/rfc8461#section-3.2))
|
|
90
|
+
* + DNS TXT record advice. Operator serves the returned `policyText`
|
|
91
|
+
* over HTTPS at `https://mta-sts.<domain>/.well-known/mta-sts.txt`
|
|
92
|
+
* and publishes the TXT record at `_mta-sts.<domain>` so peers can
|
|
93
|
+
* discover the policy version.
|
|
94
|
+
*
|
|
95
|
+
* @opts
|
|
96
|
+
* domain: string, // your mail domain, e.g. "example.com"
|
|
97
|
+
* mode: "enforce"|"testing"|"none",
|
|
98
|
+
* mxHosts: string[], // your MX server hostnames (wildcards `*.mx.` allowed per §3.2.1)
|
|
99
|
+
* maxAgeSec: number, // policy TTL — RFC 8461 §3.2 SHOULD be ≥ 604800 (1 week)
|
|
100
|
+
* policyId: string?, // optional; defaults to ISO 8601 timestamp
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* var rv = b.mail.deploy.mtaStsPublish({
|
|
104
|
+
* domain: "example.com",
|
|
105
|
+
* mode: "enforce",
|
|
106
|
+
* mxHosts: ["mx1.example.com", "mx2.example.com"],
|
|
107
|
+
* maxAgeSec: 604800,
|
|
108
|
+
* });
|
|
109
|
+
* rv.policyText; // → multi-line MTA-STS policy
|
|
110
|
+
* rv.dnsTxtRecord; // → "v=STSv1; id=20260516T120000Z;"
|
|
111
|
+
* rv.policyPath; // → "/.well-known/mta-sts.txt"
|
|
112
|
+
* rv.dnsTxtName; // → "_mta-sts.example.com"
|
|
113
|
+
*/
|
|
114
|
+
function mtaStsPublish(opts) {
|
|
115
|
+
validateOpts.requireObject(opts || {}, "b.mail.deploy.mtaStsPublish",
|
|
116
|
+
MailDeployError, "mail-deploy/bad-opts");
|
|
117
|
+
if (!_domainOk(opts.domain)) {
|
|
118
|
+
throw new MailDeployError("mail-deploy/bad-domain",
|
|
119
|
+
"mtaStsPublish: opts.domain must be a valid hostname");
|
|
120
|
+
}
|
|
121
|
+
if (!STS_MODES[opts.mode]) {
|
|
122
|
+
throw new MailDeployError("mail-deploy/bad-mode",
|
|
123
|
+
"mtaStsPublish: opts.mode must be 'enforce' | 'testing' | 'none'");
|
|
124
|
+
}
|
|
125
|
+
if (!Array.isArray(opts.mxHosts) || opts.mxHosts.length === 0) {
|
|
126
|
+
throw new MailDeployError("mail-deploy/bad-mx",
|
|
127
|
+
"mtaStsPublish: opts.mxHosts must be a non-empty array");
|
|
128
|
+
}
|
|
129
|
+
if (opts.mxHosts.length > 64) { // allow:raw-byte-literal — array cap
|
|
130
|
+
throw new MailDeployError("mail-deploy/bad-mx",
|
|
131
|
+
"mtaStsPublish: opts.mxHosts must contain at most 64 entries");
|
|
132
|
+
}
|
|
133
|
+
for (var i = 0; i < opts.mxHosts.length; i++) {
|
|
134
|
+
var m = opts.mxHosts[i];
|
|
135
|
+
if (typeof m !== "string" || m.length === 0 || m.length > 253) { // allow:raw-byte-literal — RFC 1035 cap
|
|
136
|
+
throw new MailDeployError("mail-deploy/bad-mx",
|
|
137
|
+
"mtaStsPublish: opts.mxHosts[" + i + "] invalid");
|
|
138
|
+
}
|
|
139
|
+
// Allow wildcard `*.mx.example.com` per RFC 8461 §3.2.1.
|
|
140
|
+
var bare = m.charCodeAt(0) === 0x2A && m.charCodeAt(1) === 0x2E ? m.slice(2) : m;
|
|
141
|
+
if (!_domainOk(bare)) {
|
|
142
|
+
throw new MailDeployError("mail-deploy/bad-mx",
|
|
143
|
+
"mtaStsPublish: opts.mxHosts[" + i + "] not a valid hostname");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!numericBounds.isPositiveFiniteInt(opts.maxAgeSec)) {
|
|
147
|
+
throw new MailDeployError("mail-deploy/bad-max-age",
|
|
148
|
+
"mtaStsPublish: opts.maxAgeSec must be a positive integer");
|
|
149
|
+
}
|
|
150
|
+
if (opts.maxAgeSec > 31557600) { // allow:raw-time-literal — 1 year in seconds (RFC 8461 §3.2 max_age unit) // allow:raw-byte-literal — same numeric, no byte semantic
|
|
151
|
+
throw new MailDeployError("mail-deploy/bad-max-age",
|
|
152
|
+
"mtaStsPublish: opts.maxAgeSec exceeds 1 year (RFC 8461 §3.2 SHOULD ≤ 31557600)");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// RFC 8461 §3.2 policy text uses CRLF.
|
|
156
|
+
var lines = [];
|
|
157
|
+
lines.push("version: STSv1");
|
|
158
|
+
lines.push("mode: " + opts.mode);
|
|
159
|
+
for (var j = 0; j < opts.mxHosts.length; j++) {
|
|
160
|
+
lines.push("mx: " + opts.mxHosts[j]);
|
|
161
|
+
}
|
|
162
|
+
lines.push("max_age: " + opts.maxAgeSec);
|
|
163
|
+
var policyText = lines.join("\r\n") + "\r\n";
|
|
164
|
+
|
|
165
|
+
// RFC 8461 §3.1 — DNS TXT record carries the policy version (id).
|
|
166
|
+
// Operator updates `id` whenever they re-publish a different policy
|
|
167
|
+
// so peers can detect the change without re-fetching every fetch.
|
|
168
|
+
var policyId;
|
|
169
|
+
if (typeof opts.policyId === "string" && opts.policyId.length > 0) {
|
|
170
|
+
if (!/^[a-zA-Z0-9_-]{1,32}$/.test(opts.policyId)) { // allow:raw-byte-literal — RFC 8461 §3.1 token shape
|
|
171
|
+
throw new MailDeployError("mail-deploy/bad-policy-id",
|
|
172
|
+
"mtaStsPublish: opts.policyId must match [a-zA-Z0-9_-]{1,32}");
|
|
173
|
+
}
|
|
174
|
+
policyId = opts.policyId;
|
|
175
|
+
} else {
|
|
176
|
+
// ISO 8601 timestamp w/o punctuation = unique-by-second.
|
|
177
|
+
policyId = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 16); // allow:raw-byte-literal — yyyymmddhhmmssms
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
policyText: policyText,
|
|
182
|
+
policyPath: "/.well-known/mta-sts.txt",
|
|
183
|
+
dnsTxtName: "_mta-sts." + opts.domain,
|
|
184
|
+
dnsTxtRecord: "v=STSv1; id=" + policyId + ";",
|
|
185
|
+
policyId: policyId,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @primitive b.mail.deploy.danePublish
|
|
191
|
+
* @signature b.mail.deploy.danePublish(opts)
|
|
192
|
+
* @since 0.9.56
|
|
193
|
+
* @status stable
|
|
194
|
+
*
|
|
195
|
+
* Generate a TLSA record string ([RFC 7672](https://www.rfc-editor.org/rfc/rfc7672)
|
|
196
|
+
* + [RFC 6698](https://www.rfc-editor.org/rfc/rfc6698)) for an MX
|
|
197
|
+
* host's TLS certificate. Computes the SHA-256 SubjectPublicKeyInfo
|
|
198
|
+
* hash of the operator-supplied cert PEM (DANE-EE matching type 1) —
|
|
199
|
+
* the recommended posture per RFC 7672 §3.1.3 because it survives
|
|
200
|
+
* intermediate-CA changes as long as the leaf key stays stable.
|
|
201
|
+
*
|
|
202
|
+
* @opts
|
|
203
|
+
* certPem: string, // PEM cert text
|
|
204
|
+
* mxHost: string, // e.g. "mx1.example.com"
|
|
205
|
+
* port: number?, // default 25 (RFC 7672 §3.1)
|
|
206
|
+
* usage: number?, // 3 (DANE-EE) | 2 (DANE-TA) | 1 (PKIX-EE) | 0 (PKIX-TA); default 3
|
|
207
|
+
* selector: number?, // 1 (SPKI) | 0 (cert); default 1
|
|
208
|
+
* matchType: number?, // 1 (SHA-256) | 2 (SHA-512); default 1
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* var rv = b.mail.deploy.danePublish({
|
|
212
|
+
* certPem: fs.readFileSync("/etc/letsencrypt/live/mx1/cert.pem", "utf8"),
|
|
213
|
+
* mxHost: "mx1.example.com",
|
|
214
|
+
* });
|
|
215
|
+
* rv.dnsName; // → "_25._tcp.mx1.example.com"
|
|
216
|
+
* rv.record; // → "3 1 1 <64-hex>"
|
|
217
|
+
* rv.zoneLine; // → "_25._tcp.mx1.example.com. IN TLSA 3 1 1 <64-hex>"
|
|
218
|
+
*/
|
|
219
|
+
function danePublish(opts) {
|
|
220
|
+
validateOpts.requireObject(opts || {}, "b.mail.deploy.danePublish",
|
|
221
|
+
MailDeployError, "mail-deploy/bad-opts");
|
|
222
|
+
validateOpts.requireNonEmptyString(opts.certPem,
|
|
223
|
+
"b.mail.deploy.danePublish: opts.certPem", MailDeployError, "mail-deploy/bad-cert");
|
|
224
|
+
if (opts.certPem.length > 65536) { // allow:raw-byte-literal — sanity cap on PEM input
|
|
225
|
+
throw new MailDeployError("mail-deploy/bad-cert",
|
|
226
|
+
"danePublish: opts.certPem too large");
|
|
227
|
+
}
|
|
228
|
+
if (!_domainOk(opts.mxHost)) {
|
|
229
|
+
throw new MailDeployError("mail-deploy/bad-mx-host",
|
|
230
|
+
"danePublish: opts.mxHost must be a valid hostname");
|
|
231
|
+
}
|
|
232
|
+
var port = opts.port === undefined ? 25 : opts.port; // allow:raw-byte-literal — RFC 7672 §3.1 default port
|
|
233
|
+
if (!numericBounds.isPositiveFiniteInt(port) || port > 65535) { // allow:raw-byte-literal — IANA port range
|
|
234
|
+
throw new MailDeployError("mail-deploy/bad-port",
|
|
235
|
+
"danePublish: opts.port must be 1..65535");
|
|
236
|
+
}
|
|
237
|
+
var usage = opts.usage === undefined ? 3 : opts.usage; // allow:raw-byte-literal — DANE-EE
|
|
238
|
+
var selector = opts.selector === undefined ? 1 : opts.selector; // allow:raw-byte-literal — SPKI
|
|
239
|
+
var matchType = opts.matchType === undefined ? 1 : opts.matchType; // allow:raw-byte-literal — SHA-256
|
|
240
|
+
if ([0, 1, 2, 3].indexOf(usage) === -1) {
|
|
241
|
+
throw new MailDeployError("mail-deploy/bad-usage",
|
|
242
|
+
"danePublish: opts.usage must be 0|1|2|3 (RFC 6698 §2.1.1)");
|
|
243
|
+
}
|
|
244
|
+
if ([0, 1].indexOf(selector) === -1) {
|
|
245
|
+
throw new MailDeployError("mail-deploy/bad-selector",
|
|
246
|
+
"danePublish: opts.selector must be 0|1 (RFC 6698 §2.1.2)");
|
|
247
|
+
}
|
|
248
|
+
if ([1, 2].indexOf(matchType) === -1) {
|
|
249
|
+
throw new MailDeployError("mail-deploy/bad-match-type",
|
|
250
|
+
"danePublish: opts.matchType must be 1|2 (RFC 6698 §2.1.3; matchType 0 'exact' refused — record bloat)");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Parse cert PEM via node:crypto X509Certificate, extract the bytes
|
|
254
|
+
// we hash. selector=0 → full DER; selector=1 → SubjectPublicKeyInfo.
|
|
255
|
+
var x509;
|
|
256
|
+
try {
|
|
257
|
+
x509 = new nodeCrypto.X509Certificate(opts.certPem);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
throw new MailDeployError("mail-deploy/bad-cert",
|
|
260
|
+
"danePublish: cert PEM did not parse: " + (e && e.message ? e.message : String(e)));
|
|
261
|
+
}
|
|
262
|
+
var bytes;
|
|
263
|
+
if (selector === 0) {
|
|
264
|
+
bytes = x509.raw;
|
|
265
|
+
} else {
|
|
266
|
+
// SPKI extraction — node:crypto X509Certificate.publicKey.export.
|
|
267
|
+
var spki = x509.publicKey.export({ type: "spki", format: "der" });
|
|
268
|
+
bytes = spki;
|
|
269
|
+
}
|
|
270
|
+
var algo = matchType === 1 ? "sha256" : "sha512";
|
|
271
|
+
var hashHex = nodeCrypto.createHash(algo).update(bytes).digest("hex");
|
|
272
|
+
var record = usage + " " + selector + " " + matchType + " " + hashHex;
|
|
273
|
+
var dnsName = "_" + port + "._tcp." + opts.mxHost;
|
|
274
|
+
return {
|
|
275
|
+
dnsName: dnsName,
|
|
276
|
+
record: record,
|
|
277
|
+
zoneLine: dnsName + ". IN TLSA " + record,
|
|
278
|
+
usage: usage,
|
|
279
|
+
selector: selector,
|
|
280
|
+
matchType: matchType,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @primitive b.mail.deploy.autoConfigXml
|
|
286
|
+
* @signature b.mail.deploy.autoConfigXml(opts)
|
|
287
|
+
* @since 0.9.56
|
|
288
|
+
* @status stable
|
|
289
|
+
*
|
|
290
|
+
* Generate Thunderbird's `autoconfig.<domain>/mail/config-v1.1.xml`
|
|
291
|
+
* payload. Thunderbird checks this URL when a user types their
|
|
292
|
+
* email address into the new-account wizard; serving the XML
|
|
293
|
+
* eliminates the per-user IMAP / SMTP host + port + auth-method
|
|
294
|
+
* data entry that mail clients otherwise demand.
|
|
295
|
+
*
|
|
296
|
+
* The endpoint format is Mozilla-convention rather than RFC, but
|
|
297
|
+
* Outlook, Apple Mail's Mail.app, and Evolution all read the same
|
|
298
|
+
* file when present.
|
|
299
|
+
*
|
|
300
|
+
* @opts
|
|
301
|
+
* domain: string, // e.g. "example.com"
|
|
302
|
+
* displayName: string?, // brand label; defaults to domain
|
|
303
|
+
* imap: { host, port, socketType?, username? }, // optional
|
|
304
|
+
* pop3: { host, port, socketType?, username? }, // optional
|
|
305
|
+
* smtp: { host, port, socketType?, username? }, // optional
|
|
306
|
+
* jmap: { url }?, // optional — JMAP-aware clients
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* var xml = b.mail.deploy.autoConfigXml({
|
|
310
|
+
* domain: "example.com",
|
|
311
|
+
* imap: { host: "imap.example.com", port: 993, socketType: "SSL" },
|
|
312
|
+
* smtp: { host: "smtp.example.com", port: 587, socketType: "STARTTLS" },
|
|
313
|
+
* });
|
|
314
|
+
* // Serve at `https://autoconfig.example.com/mail/config-v1.1.xml`
|
|
315
|
+
*/
|
|
316
|
+
function autoConfigXml(opts) {
|
|
317
|
+
validateOpts.requireObject(opts || {}, "b.mail.deploy.autoConfigXml",
|
|
318
|
+
MailDeployError, "mail-deploy/bad-opts");
|
|
319
|
+
if (!_domainOk(opts.domain)) {
|
|
320
|
+
throw new MailDeployError("mail-deploy/bad-domain",
|
|
321
|
+
"autoConfigXml: opts.domain must be a valid hostname");
|
|
322
|
+
}
|
|
323
|
+
var brand = typeof opts.displayName === "string" && opts.displayName.length > 0 ?
|
|
324
|
+
opts.displayName : opts.domain;
|
|
325
|
+
if (brand.length > 256) { // allow:raw-byte-literal — DOM attr cap
|
|
326
|
+
throw new MailDeployError("mail-deploy/bad-displayName",
|
|
327
|
+
"autoConfigXml: opts.displayName too long");
|
|
328
|
+
}
|
|
329
|
+
// Per Mozilla autoconfig config-v1.1 spec
|
|
330
|
+
// (https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat),
|
|
331
|
+
// the `type` attribute on `incomingServer` / `outgoingServer` carries
|
|
332
|
+
// the protocol name (`imap` / `pop3` / `smtp`), not the direction. The
|
|
333
|
+
// `incomingServer` / `outgoingServer` element name itself signals
|
|
334
|
+
// direction; the attribute disambiguates between IMAP- and POP3-
|
|
335
|
+
// shaped incoming connections.
|
|
336
|
+
function _server(element, protocol, cfg) {
|
|
337
|
+
if (!cfg) return "";
|
|
338
|
+
if (!_domainOk(cfg.host)) {
|
|
339
|
+
throw new MailDeployError("mail-deploy/bad-host",
|
|
340
|
+
"autoConfigXml: opts." + protocol + ".host invalid");
|
|
341
|
+
}
|
|
342
|
+
if (!numericBounds.isPositiveFiniteInt(cfg.port) || cfg.port > 65535) { // allow:raw-byte-literal — IANA port
|
|
343
|
+
throw new MailDeployError("mail-deploy/bad-port",
|
|
344
|
+
"autoConfigXml: opts." + protocol + ".port invalid");
|
|
345
|
+
}
|
|
346
|
+
var socketType = cfg.socketType === "STARTTLS" || cfg.socketType === "plain" ?
|
|
347
|
+
cfg.socketType : "SSL";
|
|
348
|
+
var userTok = typeof cfg.username === "string" && cfg.username.length > 0 ?
|
|
349
|
+
cfg.username : "%EMAILADDRESS%";
|
|
350
|
+
return "" +
|
|
351
|
+
" <" + element + " type=\"" + protocol + "\">\n" +
|
|
352
|
+
" <hostname>" + _xmlEscape(cfg.host) + "</hostname>\n" +
|
|
353
|
+
" <port>" + cfg.port + "</port>\n" +
|
|
354
|
+
" <socketType>" + socketType + "</socketType>\n" +
|
|
355
|
+
" <username>" + _xmlEscape(userTok) + "</username>\n" +
|
|
356
|
+
" <authentication>password-cleartext</authentication>\n" +
|
|
357
|
+
" </" + element + ">\n";
|
|
358
|
+
}
|
|
359
|
+
// JMAP-aware clients read a different element (`mailproxy` /
|
|
360
|
+
// `jmapServer` per the Mozilla draft + Fastmail convention).
|
|
361
|
+
function _jmapServer(cfg) {
|
|
362
|
+
if (!cfg) return "";
|
|
363
|
+
if (typeof cfg.url !== "string" || cfg.url.length === 0 || cfg.url.length > 1024) { // allow:raw-byte-literal — URL cap
|
|
364
|
+
throw new MailDeployError("mail-deploy/bad-jmap-url",
|
|
365
|
+
"autoConfigXml: opts.jmap.url must be a non-empty string");
|
|
366
|
+
}
|
|
367
|
+
// Refuse control bytes / quote in the URL.
|
|
368
|
+
for (var k = 0; k < cfg.url.length; k++) {
|
|
369
|
+
var c = cfg.url.charCodeAt(k);
|
|
370
|
+
if (c < 0x20 || c === 0x7F || c === 0x22) { // allow:raw-byte-literal — C0 / DEL / "
|
|
371
|
+
throw new MailDeployError("mail-deploy/bad-jmap-url",
|
|
372
|
+
"autoConfigXml: opts.jmap.url contains control byte");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return "" +
|
|
376
|
+
" <incomingServer type=\"jmap\">\n" +
|
|
377
|
+
" <url>" + _xmlEscape(cfg.url) + "</url>\n" +
|
|
378
|
+
" <username>%EMAILADDRESS%</username>\n" +
|
|
379
|
+
" <authentication>OAuth2</authentication>\n" +
|
|
380
|
+
" </incomingServer>\n";
|
|
381
|
+
}
|
|
382
|
+
var incoming = "";
|
|
383
|
+
if (opts.imap) incoming += _server("incomingServer", "imap", opts.imap);
|
|
384
|
+
if (opts.pop3) incoming += _server("incomingServer", "pop3", opts.pop3);
|
|
385
|
+
if (opts.jmap) incoming += _jmapServer(opts.jmap);
|
|
386
|
+
if (!incoming) {
|
|
387
|
+
throw new MailDeployError("mail-deploy/bad-opts",
|
|
388
|
+
"autoConfigXml: at least one of opts.imap / opts.pop3 / opts.jmap required");
|
|
389
|
+
}
|
|
390
|
+
var outgoing = opts.smtp ? _server("outgoingServer", "smtp", opts.smtp) : "";
|
|
391
|
+
|
|
392
|
+
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
|
393
|
+
"<clientConfig version=\"1.1\">\n" +
|
|
394
|
+
" <emailProvider id=\"" + _xmlEscape(opts.domain) + "\">\n" +
|
|
395
|
+
" <domain>" + _xmlEscape(opts.domain) + "</domain>\n" +
|
|
396
|
+
" <displayName>" + _xmlEscape(brand) + "</displayName>\n" +
|
|
397
|
+
" <displayShortName>" + _xmlEscape(brand) + "</displayShortName>\n" +
|
|
398
|
+
incoming +
|
|
399
|
+
outgoing +
|
|
400
|
+
" </emailProvider>\n" +
|
|
401
|
+
"</clientConfig>\n";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* @primitive b.mail.deploy.autoDiscoverXml
|
|
406
|
+
* @signature b.mail.deploy.autoDiscoverXml(opts)
|
|
407
|
+
* @since 0.9.56
|
|
408
|
+
* @status stable
|
|
409
|
+
*
|
|
410
|
+
* Generate Outlook's `autodiscover/autodiscover.xml` response payload.
|
|
411
|
+
* Outlook POSTs an XML request to
|
|
412
|
+
* `https://autodiscover.<domain>/autodiscover/autodiscover.xml` with
|
|
413
|
+
* the user's email; the response declares IMAP + SMTP host / port /
|
|
414
|
+
* socket settings. MS-OXDISCO + MS-OXDSCLI (open spec).
|
|
415
|
+
*
|
|
416
|
+
* @opts
|
|
417
|
+
* email: string, // operator-extracted from the POST body
|
|
418
|
+
* imap: { host, port, ssl? }, // optional
|
|
419
|
+
* pop3: { host, port, ssl? }, // optional
|
|
420
|
+
* smtp: { host, port, ssl? }, // optional
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* var xml = b.mail.deploy.autoDiscoverXml({
|
|
424
|
+
* email: "alice@example.com",
|
|
425
|
+
* imap: { host: "imap.example.com", port: 993, ssl: true },
|
|
426
|
+
* smtp: { host: "smtp.example.com", port: 465, ssl: true },
|
|
427
|
+
* });
|
|
428
|
+
*/
|
|
429
|
+
function autoDiscoverXml(opts) {
|
|
430
|
+
validateOpts.requireObject(opts || {}, "b.mail.deploy.autoDiscoverXml",
|
|
431
|
+
MailDeployError, "mail-deploy/bad-opts");
|
|
432
|
+
if (typeof opts.email !== "string" || opts.email.length === 0 || opts.email.length > 254) { // allow:raw-byte-literal — RFC 5321 cap
|
|
433
|
+
throw new MailDeployError("mail-deploy/bad-email",
|
|
434
|
+
"autoDiscoverXml: opts.email must be a non-empty string");
|
|
435
|
+
}
|
|
436
|
+
// Refuse CR / LF / NUL / control bytes in email (XML injection class).
|
|
437
|
+
for (var i = 0; i < opts.email.length; i++) {
|
|
438
|
+
var c = opts.email.charCodeAt(i);
|
|
439
|
+
if (c < 0x20 || c === 0x7F) { // allow:raw-byte-literal — C0 / DEL
|
|
440
|
+
throw new MailDeployError("mail-deploy/bad-email",
|
|
441
|
+
"autoDiscoverXml: opts.email contains control byte");
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function _proto(kind, cfg) {
|
|
445
|
+
if (!cfg) return "";
|
|
446
|
+
if (!_domainOk(cfg.host)) {
|
|
447
|
+
throw new MailDeployError("mail-deploy/bad-host",
|
|
448
|
+
"autoDiscoverXml: opts." + kind.toLowerCase() + ".host invalid");
|
|
449
|
+
}
|
|
450
|
+
if (!numericBounds.isPositiveFiniteInt(cfg.port) || cfg.port > 65535) { // allow:raw-byte-literal — IANA port
|
|
451
|
+
throw new MailDeployError("mail-deploy/bad-port",
|
|
452
|
+
"autoDiscoverXml: opts." + kind.toLowerCase() + ".port invalid");
|
|
453
|
+
}
|
|
454
|
+
var ssl = cfg.ssl === false ? "off" : "on";
|
|
455
|
+
return "" +
|
|
456
|
+
" <Protocol>\n" +
|
|
457
|
+
" <Type>" + kind + "</Type>\n" +
|
|
458
|
+
" <Server>" + _xmlEscape(cfg.host) + "</Server>\n" +
|
|
459
|
+
" <Port>" + cfg.port + "</Port>\n" +
|
|
460
|
+
" <SSL>" + ssl + "</SSL>\n" +
|
|
461
|
+
" <SPA>off</SPA>\n" +
|
|
462
|
+
" <Encryption>" + (ssl === "on" ? "SSL" : "None") + "</Encryption>\n" +
|
|
463
|
+
" <AuthRequired>on</AuthRequired>\n" +
|
|
464
|
+
" </Protocol>\n";
|
|
465
|
+
}
|
|
466
|
+
var protos = "";
|
|
467
|
+
if (opts.imap) protos += _proto("IMAP", opts.imap);
|
|
468
|
+
if (opts.pop3) protos += _proto("POP3", opts.pop3);
|
|
469
|
+
if (opts.smtp) protos += _proto("SMTP", opts.smtp);
|
|
470
|
+
if (!protos) {
|
|
471
|
+
throw new MailDeployError("mail-deploy/bad-opts",
|
|
472
|
+
"autoDiscoverXml: at least one of opts.imap / opts.pop3 / opts.smtp required");
|
|
473
|
+
}
|
|
474
|
+
return "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
|
|
475
|
+
"<Autodiscover xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006\">\n" +
|
|
476
|
+
" <Response xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a\">\n" +
|
|
477
|
+
" <Account>\n" +
|
|
478
|
+
" <AccountType>email</AccountType>\n" +
|
|
479
|
+
" <Action>settings</Action>\n" +
|
|
480
|
+
protos +
|
|
481
|
+
" </Account>\n" +
|
|
482
|
+
" </Response>\n" +
|
|
483
|
+
"</Autodiscover>\n";
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
module.exports = {
|
|
487
|
+
mtaStsPublish: mtaStsPublish,
|
|
488
|
+
danePublish: danePublish,
|
|
489
|
+
autoConfigXml: autoConfigXml,
|
|
490
|
+
autoDiscoverXml: autoDiscoverXml,
|
|
491
|
+
MailDeployError: MailDeployError,
|
|
492
|
+
};
|