@blamejs/core 0.9.49 → 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.
Files changed (77) hide show
  1. package/CHANGELOG.md +951 -908
  2. package/index.js +25 -0
  3. package/lib/_test/crypto-fixtures.js +67 -0
  4. package/lib/agent-event-bus.js +52 -6
  5. package/lib/agent-idempotency.js +169 -16
  6. package/lib/agent-orchestrator.js +263 -9
  7. package/lib/agent-posture-chain.js +163 -5
  8. package/lib/agent-saga.js +146 -16
  9. package/lib/agent-snapshot.js +349 -19
  10. package/lib/agent-stream.js +34 -2
  11. package/lib/agent-tenant.js +179 -23
  12. package/lib/agent-trace.js +84 -21
  13. package/lib/auth/aal.js +8 -1
  14. package/lib/auth/ciba.js +6 -1
  15. package/lib/auth/dpop.js +7 -2
  16. package/lib/auth/fal.js +17 -8
  17. package/lib/auth/jwt-external.js +128 -4
  18. package/lib/auth/oauth.js +232 -10
  19. package/lib/auth/oid4vci.js +67 -7
  20. package/lib/auth/openid-federation.js +71 -25
  21. package/lib/auth/passkey.js +140 -6
  22. package/lib/auth/sd-jwt-vc.js +67 -5
  23. package/lib/circuit-breaker.js +10 -2
  24. package/lib/compliance.js +176 -8
  25. package/lib/crypto-field.js +114 -14
  26. package/lib/crypto.js +216 -20
  27. package/lib/db.js +1 -0
  28. package/lib/guard-jmap.js +321 -0
  29. package/lib/guard-managesieve-command.js +566 -0
  30. package/lib/guard-pop3-command.js +317 -0
  31. package/lib/guard-smtp-command.js +58 -3
  32. package/lib/mail-agent.js +20 -7
  33. package/lib/mail-arc-sign.js +12 -8
  34. package/lib/mail-auth.js +323 -34
  35. package/lib/mail-crypto-pgp.js +934 -0
  36. package/lib/mail-crypto-smime.js +340 -0
  37. package/lib/mail-crypto.js +108 -0
  38. package/lib/mail-dav.js +1224 -0
  39. package/lib/mail-deploy.js +492 -0
  40. package/lib/mail-dkim.js +431 -26
  41. package/lib/mail-journal.js +435 -0
  42. package/lib/mail-scan.js +502 -0
  43. package/lib/mail-server-imap.js +64 -26
  44. package/lib/mail-server-jmap.js +488 -0
  45. package/lib/mail-server-managesieve.js +853 -0
  46. package/lib/mail-server-mx.js +40 -30
  47. package/lib/mail-server-pop3.js +836 -0
  48. package/lib/mail-server-rate-limit.js +13 -0
  49. package/lib/mail-server-submission.js +70 -24
  50. package/lib/mail-server-tls.js +445 -0
  51. package/lib/mail-sieve.js +557 -0
  52. package/lib/mail-spam-score.js +284 -0
  53. package/lib/mail.js +99 -0
  54. package/lib/metrics.js +80 -3
  55. package/lib/middleware/dpop.js +58 -3
  56. package/lib/middleware/idempotency-key.js +255 -42
  57. package/lib/middleware/protected-resource-metadata.js +114 -2
  58. package/lib/network-dns-resolver.js +33 -0
  59. package/lib/network-tls.js +46 -0
  60. package/lib/outbox.js +62 -12
  61. package/lib/pqc-agent.js +13 -5
  62. package/lib/retry.js +23 -9
  63. package/lib/router.js +23 -1
  64. package/lib/safe-ical.js +634 -0
  65. package/lib/safe-icap.js +502 -0
  66. package/lib/safe-mime.js +15 -0
  67. package/lib/safe-sieve.js +684 -0
  68. package/lib/safe-smtp.js +57 -0
  69. package/lib/safe-url.js +37 -0
  70. package/lib/safe-vcard.js +473 -0
  71. package/lib/self-update-standalone-verifier.js +32 -3
  72. package/lib/self-update.js +153 -33
  73. package/lib/vendor/MANIFEST.json +161 -156
  74. package/lib/vendor-data.js +127 -9
  75. package/lib/vex.js +324 -59
  76. package/package.json +1 -1
  77. 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, "&amp;")
76
+ .replace(/</g, "&lt;")
77
+ .replace(/>/g, "&gt;")
78
+ .replace(/"/g, "&quot;")
79
+ .replace(/'/g, "&apos;");
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
+ };