Outbound delivery
Turnkey outbound SMTP composer. Wraps the discovery chain (MX-lookup → MTA-STS-fetch + MX-allowlist match → DANE TLSA query → REQUIRETLS handshake hint) around the existing per-host b.mail.transports.smtp wire-layer, plus deferred-retry scheduling for transient failures and RFC 3464 DSN generation for permanent ones.
Operators no longer have to glue these pieces by hand:
var deliver = b.mail.send.deliver.create({ hostname: "mta1.example.com", policy: { mtaSts: "enforce", dane: "opportunistic" }, dsn: { from: "mailer-daemon@example.com", onPermanentFailure: function (env, hist) { ... } }, resolver: b.network.dns.resolver.create({ ... }), });
var result = await deliver({ from: "ops@example.com", to: ["alice@recipient.com", "bob@other.com"], rfc822: messageBuffer, requireTls: true, }); // → { delivered: [{ recipient, mxHost, tlsProtocol, ... }], // deferred: [{ recipient, mxHost, reason, retryAfterMs }], // failed: [{ recipient, reason, dsnSent }] } // deferred.mxHost is the receiver that refused, or null when the // deferral happened before any host was reached (an MX lookup that // failed). A queue view without it cannot say which peer to chase.
Composes: - b.network.smtp.policy.mtaSts.fetch + .matchMx → RFC 8461 enforcement - b.network.smtp.policy.dane.tlsa + .verifyChain → RFC 7672 TLSA query and peer authentication - b.network.dns.resolver (operator-supplied) → caching + DoH posture - b.mail.transports.smtp → SMTP wire layer - b.mail.requireTls → RFC 8689 REQUIRETLS - b.mailBounce-style RFC 3464 DSN generation → permanent-failure report-mail - b.audit → mail.send.deliver.* events - b.safeAsync.repeating + operator's queue → retry scheduling (deferred deliveries re-enter via the retry.scheduleRetry callback)
The deferred-retry surface is operator-side: this primitive classifies a recipient's outcome as "deferred" and emits a retryAfterMs budget; the operator's queue / scheduler re-invokes deliver for the deferred recipient after that elapses. The primitive does NOT own a background scheduler — that ownership lives with the operator's job-runner so a single deferred-delivery tick can't pin a long-lived process.
b.mail.send.deliver.create(opts) #
{
hostname: string, // required — local hostname for HELO/EHLO + DSN Reporting-MTA
port: number, // default 25 (IANA SMTP, RFC 5321) — set 587 (RFC 6409 submission) or 465 (RFC 8314 implicit-TLS) for a smarthost relay
resolver: object | null, // optional — b.network.dns.resolver handle; falls back to node:dns when omitted.
// Every lookup made on the sender's behalf goes through it: the MX
// records, the `_mta-sts` TXT record and the DANE TLSA records
policy: {
mtaSts: "enforce" | "testing" | "off", // default "enforce" — RFC 8461 posture
dane: "opportunistic" | "enforce" | "off", // default "opportunistic" — RFC 7672
dnssecValidated: boolean, // default false — assert the resolver DNSSEC-validates; required by dane "enforce"
},
retry: {
maxAttempts: number, // default backoffMs.length + 1, so every rung is used
backoffMs: Array, // default [1m, 5m, 15m, 1h, 4h, 8h, 12h, 24h, 24h, 24h] — gives up after ~4 days (RFC 5321 §4.5.4.1)
},
dsn: {
from: string, // required when dsn.onPermanentFailure is set
onPermanentFailure: function (envelope, result, dsnMessage) → Promise,
},
timeouts: {
mxLookupMs: number, // default 10s
perHostMs: number, // default 60s
},
audit: boolean, // default true
}
Build a turnkey delivery handle. Returns a deliver(envelope) function that takes a single multi-recipient envelope, resolves MX records per recipient domain, applies the operator's configured MTA-STS / DANE policy, attempts delivery via b.mail.transports.smtp, and returns a per-recipient outcome split into delivered / deferred / failed arrays.
Deferred recipients carry retryAfterMs budgets the operator's queue / scheduler honors by re-invoking deliver for that subset after the budget elapses. The primitive does not own a background scheduler — operator job-runner owns the retry lifecycle.
DANE (RFC 7672) authenticates the peer, not just its DNS. When the peer publishes TLSA records, they are fetched and the certificate chain it presents during the handshake is matched against them; a peer whose chain matches none of its own records is refused and the next MX is tried.
The TLSA lookup goes through opts.resolver when one is supplied, because the DNSSEC assertion below is a statement about THAT resolver. A resolver that cannot answer TLSA is refused rather than bypassed.
Because RFC 7672 §1.3 forbids using records that were not DNSSEC-validated and node:dns does not expose the AD bit, only the operator can say whether their resolver validates. policy.dnssecValidated: true is that statement, and dane: "enforce" requires it: without it every peer that publishes TLSA would be refused while every peer that publishes none was delivered to. Under "opportunistic" the records are unusable without it, so none are fetched.
Failed recipients trigger DSN composition: a RFC 3464 multipart/ report message is built per failed recipient and handed to the operator-supplied dsn.onPermanentFailure(envelope, recipientResult, dsnMessage) callback. The callback is responsible for delivering the DSN itself (typically by re-entering the same deliver handle with the original sender as recipient — but operators who want a separate transport for DSNs wire that here).
var deliver = b.mail.send.deliver.create({
hostname: "mta1.example.com",
policy: { mtaSts: "enforce", dane: "opportunistic" },
dsn: { from: "mailer-daemon@example.com",
onPermanentFailure: function (env, res, dsn) {
return deliver({ from: env.from, to: [env.from], rfc822: Buffer.from(dsn) });
} },
});
var result = await deliver({
from: "ops@example.com",
to: ["alice@recipient.com"],
rfc822: messageBuffer,
});
typeof result.delivered; // → "object" (array)
typeof result.deferred; // → "object" (array)
typeof result.failed; // → "object" (array)