Mail deployment helpers
Operator-deployment helpers for standing up a blamejs mail server. Generates the policy text + DNS records + client auto-discovery XML every deployment needs alongside the wire- protocol primitives. Pairs with existing verifiers (b.network.smtp.policy carries the inbound MTA-STS / TLS-RPT evaluation logic shipped pre-v0.9.46; b.mail.bimi carries the inbound BIMI trust-anchor verifier) so the publish-side helpers stay thin and the operator runs one vocabulary across both sides.
Surface: - b.mail.deploy.mtaStsPublish(opts) — RFC 8461 §3.2 /.well-known/mta-sts.txt policy text + DNS TXT record advice + DNS record-name advice. Pairs with the inbound MTA-STS verifier on the receiving side. - b.mail.deploy.danePublish(opts) — RFC 7672 + RFC 6698 TLSA record generator. Computes SHA-256 SubjectPublicKeyInfo hash from an operator-supplied PEM cert, returns the TLSA record string for the operator's DNS zone. - b.mail.deploy.autoConfigXml(opts) — Thunderbird's autoconfig.example.com/mail/config-v1.1.xml shape. RFC-less (Mozilla convention) but documented at https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat - b.mail.deploy.autoDiscoverXml(opts) — Outlook's autodiscover.example.com/autodiscover/autodiscover.xml response shape. MS-OXDSCLI Section 5 + MS-OXDISCO.
The XML generators emit single-string output the operator wires into b.staticServe (mta-sts.txt + autoconfig.xml) or a route handler (autodiscover, which is request-conditional). No new network surface — these are pure deterministic functions.
b.mail.deploy.mtaStsPublish(opts) #
{
domain: string, // your mail domain, e.g. "example.com"
mode: "enforce"|"testing"|"none",
mxHosts: string[], // your MX server hostnames (wildcards `*.mx.` allowed per §3.2.1)
maxAgeSec: number, // policy TTL — RFC 8461 §3.2 SHOULD be ≥ 604800 (1 week)
policyId: string?, // optional; defaults to ISO 8601 timestamp
}
Generate the MTA-STS policy file ([RFC 8461 §3.2](https://www.rfc-editor.org/rfc/rfc8461#section-3.2)) + DNS TXT record advice. Operator serves the returned policyText over HTTPS at https://mta-sts. and publishes the TXT record at _mta-sts. so peers can discover the policy version.
var rv = b.mail.deploy.mtaStsPublish({
domain: "example.com",
mode: "enforce",
mxHosts: ["mx1.example.com", "mx2.example.com"],
maxAgeSec: 604800,
});
rv.policyText; // → multi-line MTA-STS policy
rv.dnsTxtRecord; // → "v=STSv1; id=20260516T120000Z;"
rv.policyPath; // → "/.well-known/mta-sts.txt"
rv.dnsTxtName; // → "_mta-sts.example.com"
b.mail.deploy.danePublish(opts) #
{
certPem: string, // PEM cert text
mxHost: string, // e.g. "mx1.example.com"
port: number?, // default 25 (RFC 7672 §3.1)
usage: number?, // 3 (DANE-EE) | 2 (DANE-TA) | 1 (PKIX-EE) | 0 (PKIX-TA); default 3
selector: number?, // 1 (SPKI) | 0 (cert); default 1
matchType: number?, // 1 (SHA-256) | 2 (SHA-512); default 1
}
Generate a TLSA record string ([RFC 7672](https://www.rfc-editor.org/rfc/rfc7672) + [RFC 6698](https://www.rfc-editor.org/rfc/rfc6698)) for an MX host's TLS certificate. Computes the SHA-256 SubjectPublicKeyInfo hash of the operator-supplied cert PEM (DANE-EE matching type 1) — the recommended posture per RFC 7672 §3.1.3 because it survives intermediate-CA changes as long as the leaf key stays stable.
var rv = b.mail.deploy.danePublish({
certPem: fs.readFileSync("/etc/letsencrypt/live/mx1/cert.pem", "utf8"),
mxHost: "mx1.example.com",
});
rv.dnsName; // → "_25._tcp.mx1.example.com"
rv.record; // → "3 1 1 <64-hex>"
rv.zoneLine; // → "_25._tcp.mx1.example.com. IN TLSA 3 1 1 <64-hex>"
b.mail.deploy.autoConfigXml(opts) #
{
domain: string, // e.g. "example.com"
displayName: string?, // brand label; defaults to domain
imap: { host, port, socketType?, username? }, // optional
pop3: { host, port, socketType?, username? }, // optional
smtp: { host, port, socketType?, username? }, // optional
jmap: { url }?, // optional — JMAP-aware clients
}
Generate Thunderbird's autoconfig. payload. Thunderbird checks this URL when a user types their email address into the new-account wizard; serving the XML eliminates the per-user IMAP / SMTP host + port + auth-method data entry that mail clients otherwise demand.
The endpoint format is Mozilla-convention rather than RFC, but Outlook, Apple Mail's Mail.app, and Evolution all read the same file when present.
var xml = b.mail.deploy.autoConfigXml({
domain: "example.com",
imap: { host: "imap.example.com", port: 993, socketType: "SSL" },
smtp: { host: "smtp.example.com", port: 587, socketType: "STARTTLS" },
});
// Serve at `https://autoconfig.example.com/mail/config-v1.1.xml`
b.mail.deploy.autoDiscoverXml(opts) #
{
email: string, // operator-extracted from the POST body
imap: { host, port, ssl? }, // optional
pop3: { host, port, ssl? }, // optional
smtp: { host, port, ssl? }, // optional
}
Generate Outlook's autodiscover/autodiscover.xml response payload. Outlook POSTs an XML request to https://autodiscover. with the user's email; the response declares IMAP + SMTP host / port / socket settings. MS-OXDISCO + MS-OXDSCLI (open spec).
var xml = b.mail.deploy.autoDiscoverXml({
email: "alice@example.com",
imap: { host: "imap.example.com", port: 993, ssl: true },
smtp: { host: "smtp.example.com", port: 465, ssl: true },
});
b.mail.deploy.parseTlsRptReport(input, opts?) #
{
contentType: string, // optional — hint for gzip routing
maxCompressedBytes: number, // default TLSRPT_MAX_COMPRESSED_BYTES (4 MiB)
maxDecompressedBytes: number, // default TLSRPT_MAX_DECOMPRESSED_BYTES (32 MiB)
maxRatio: number, // default 50 (compressed:decompressed cap)
}
Parse + validate an RFC 8460 TLS-RPT aggregate report. Accepts: - Raw application/tlsrpt+json bytes (Buffer or string). - application/tlsrpt+gzip bytes (gzip magic auto-detected via 0x1f 0x8b per RFC 1952, or routed when opts.contentType names a gzip media-type).
Refusal posture: - Compressed payload > opts.maxCompressedBytes (default 4 MiB) → mail-tlsrpt/oversize-compressed. - Decompressed payload > opts.maxDecompressedBytes (default 32 MiB) → mail-tlsrpt/gunzip-bomb. - Compression ratio > opts.maxRatio (default 50:1) → mail-tlsrpt/ratio-bomb. - Malformed gzip → mail-tlsrpt/gunzip-failed. - Routes through b.guardJson.parse for proto-pollution / depth / key-count defenses before the §4.4 schema walk. - Missing REQUIRED §4.4 fields → mail-tlsrpt/bad-schema. - policies MUST be an array (RFC 8460 §4.4 erratum, even for single-policy reports).
var report = b.mail.deploy.parseTlsRptReport(reqBody, {
contentType: req.headers["content-type"],
});
// → { organization-name, date-range: {start, end}, contact-info,
// report-id, policies: [{ policy-type, policy-domain, ... }] }
b.mail.deploy.tlsRptReportSchema() #
Returns a structured RFC 8460 §4.4 schema descriptor — operator dashboards consume this to render report shape consistently. The descriptor names every required + optional field with type + cardinality + brief description. Pure function; safe to cache.
var schema = b.mail.deploy.tlsRptReportSchema();
schema.required.indexOf("report-id") !== -1; // → true
b.mail.deploy.tlsRptIngestHttp(opts) #
{
authenticate: Function, // (req) → boolean | Promise; SHA real auth boundary
trustedReporters: string[], // ADVISORY content filter on report.organization-name (operator-untrusted field)
maxCompressedBytes: number, // default 4 MiB
maxDecompressedBytes: number, // default 32 MiB
maxRatio: number, // default 50
onAccept: Function, // (report, req) → void | Promise
onRefuse: Function, // (errCode, errMessage, req) → void
audit: object, // optional b.audit handle (default: framework audit)
}
Returns an (req, res) request handler mounted at the operator's rua=https:// endpoint. Implements the receive-side of RFC 8460 §5.4:
- POST only — non-POST returns 405 with Allow: POST. - Accepts application/tlsrpt+json and application/tlsrpt+gzip (RFC 8460 §6.4-6.5 IANA media types). 415 on others. - Body size cap (default 4 MiB compressed) — 413 on exceed. - Routes the bytes through parseTlsRptReport. 400 on parse failure (with Error-Type: header naming the typed error code). 201 on accept. - Calls opts.onAccept(report, req) after successful parse. Operator's hook decides storage (most operators journal + emit a metric); the framework does NOT persist by default. - Emits a mail.tlsrpt.ingest_http audit event with posture-aware payload (organization-name, report-id, policy-domain set, session totals).
Authentication discipline: - trustedReporters is a CONTENT-SIDE soft filter — it compares the reporter's self-declared organization-name field (the report body, operator-untrusted) against the operator's allowlist. A hostile sender can forge any organization-name string to bypass it. This option is ADVISORY: a tripwire that surfaces unexpected reporter-name strings in audit, not an authentication boundary. - For real authentication, supply opts.authenticate(req) — the hook fires BEFORE parsing the body and returns truthy / falsy (or a Promise). False / falsy refuses with 401 + the mail-tlsrpt/unauthenticated audit code. Operators wire this to their mTLS-peer-cert / IP-allowlist / signed-header / reverse-proxy auth boundary. The framework intentionally does NOT couple to any specific auth scheme.
app.post("/tlsrpt", b.mail.deploy.tlsRptIngestHttp({
onAccept: function (report) {
b.journal.append({ kind: "tlsrpt", report: report });
},
}));
Last updated 2026-08-08T16:39:49.652Z by seeder.