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) #

stable0.9.56
{
  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./.well-known/mta-sts.txt 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) #

stable0.9.56
{
  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) #

stable0.9.56
{
  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./mail/config-v1.1.xml 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) #

stable0.9.56
{
  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./autodiscover/autodiscover.xml 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?) #

stable0.10.15hipaapci-dssgdprsoc2
{
  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() #

stable0.10.15

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) #

stable0.10.15hipaapci-dssgdprsoc2
{
  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.