SMTP / HTTP-API email send with multipart RFC 5322 message composition, DKIM signing on the way out, and full inbound mail- authentication parsing on the way in. Builds a multipart/alternative body for text+html, multipart/related for inline images via cid: references, multipart/mixed when attachments are present, and handles SMTPUTF8 (RFC 6531) + IDN domain Punycode (RFC 3492) for internationalized addresses.
Transports ship as b.mail.transports.*: console (stderr dev default), memory (captures to sent[] for fixtures), smtp (raw RFC 5321 over net / tls with STARTTLS, AUTH LOGIN, and PQC- friendly TLS opts), http (generic JSON-over-HTTPS for any vendor speaking that contract — Postmark / Mailgun / SES HTTP / SendGrid / Resend), resend (thin preset wiring http to the Resend API as the worked example). Operators can also pass any function or { send } object as a custom transport.
DKIM-Signature header generation lives at b.mail.dkim (rsa-sha256 default, ed25519-sha256 opt-in, dual-signer per RFC 8463 §3 for transition windows). Inbound authentication-results parsing — SPF (RFC 7208), DMARC (RFC 7489), ARC chain trust evaluation (RFC 8617) — is exposed as b.mail.spf / b.mail.dmarc / b.mail.arc / b.mail.authResults. BIMI (RFC draft) is at b.mail.bimi. RFC 8058 one-click List-Unsubscribe lives at b.mail.unsubscribe and folds in automatically when the message carries unsubscribe: { url | mailto, oneClick? }.
CAN-SPAM Act §7704 enforcement is on-by-default for instances created with commercial: true: every send refuses unless the instance supplied postalAddress AND the message exposes a functional opt-out (List-Unsubscribe header or unsubscribe.{url| mailto} on the message). The postal address auto-appends to both text and html bodies via the configured separator; operators override the html footer with footerHtml (must still contain the country + postal-code bytes — the framework refuses operator overrides that drop the legally-required address).
Validation surface uses MailError (a FrameworkError subclass) with stable codes per failure: missing-to / missing-from / missing-body / invalid-recipient / mail/transport-failed / smtp-* / http-* / resend-*. Vendor-specific presets carry their own code prefix so diagnostic logs identify the provider that rejected the message. Audit emits mail.send.success / mail.send.failure / mail.canspam.refused and records recipient COUNTS only — addresses are PII, never auto-logged.
b.mail.agent.create(opts) #
{
store: b.mailStore instance, // required
audit: b.audit namespace, // optional; defaults to b.audit
permissions: b.permissions instance, // optional; agent skips RBAC if absent (operator's choice)
posture: "hipaa"|"pci-dss"|"gdpr"|"soc2"|null,
identity: function(actorId) → { email, name } // OR object map
dispatch: { mode, queue, workerPool, queueTopic, taskTimeoutMs, queueDepthCap, vaultKeyDelivery },
}
Create the agent facade. Returns an object with read / write / sieve / identity / mdn / export / import methods. Reads stay synchronous-shaped via promises; writes audit on completion. (The queue consumer is the sibling export b.mail.agent.consumer, not a method on this object.)
var agent = b.mail.agent.create({ store: myStore });
var folders = await agent.folders({ actor: { id: "u1" } });
b.mail.agent.consumer(opts) #
{
agent: a b.mail.agent.create() instance, // required
queue: b.queue / b.queueRedis, // required
taskTopic: string, // default "mail.agent.tasks"
maxConcurrency: number, // default 4
}
Create a queue consumer that pulls mail.agent.tasks envelopes and runs them against an operator-supplied agent. Each replica runs in its own process / host for multi-host load-spreading; queue payload carries actor + posture; consumer re-validates against its local posture before unseal.
var consumer = b.mail.agent.consumer({ agent: localAgent, queue: redisQueue });
await consumer.start();
b.mail.bimi.recordShape(opts) #
{
{
logoUrl: string, // required - https:// URL to Tiny-PS SVG
vmcUrl: string?, // optional - https:// URL to VMC / CMC PEM
selector: string?, // unused at record-shape time; reserved
// for future per-selector behavior
}
}
Builds the canonical RFC 9091 BIMI TXT-record string from a logo URL and optional VMC URL. Throws on missing or non-https URLs and on control / record-separator characters in the URLs. Operators publish the returned string at default._bimi. (or the selector subdomain if they're using non-default selectors).
var rec = b.mail.bimi.recordShape({
logoUrl: "https://example.com/bimi/logo.svg",
vmcUrl: "https://example.com/bimi/cert.pem",
});
// -> "v=BIMI1; l=https://example.com/bimi/logo.svg; a=https://example.com/bimi/cert.pem"
b.mail.bimi.parseRecord(text) #
Parses a BIMI TXT record into { v, l, a }. Returns null when the text is not a v=BIMI1 record, the l= URL is missing, or the total bytes exceed the 2 KiB sanity cap. Use this when the operator already has the TXT bytes in hand (e.g. an inbound auth-results pipeline carrying the resolved record).
var rv = b.mail.bimi.parseRecord("v=BIMI1; l=https://example.com/logo.svg");
// -> { v: "BIMI1", l: "https://example.com/logo.svg", a: null }
b.mail.bimi.fetchPolicy(domain, opts?) #
{
{
selector: string?, // default "default"
dnsLookup: async (qname, type) => rows?, // operator-supplied resolver
// (DoH / cache / fixture);
// default: node:dns.resolveTxt
}
}
Resolves default._bimi. (or if opts.selector is set) and returns the parsed { v, l, a }. Returns null when no TXT record exists or no record on the resolved name parses as v=BIMI1. Operators feed the returned l= / a= URLs into fetchAndVerifyMark to retrieve the verified mark.
var pol = await b.mail.bimi.fetchPolicy("example.com");
if (pol && pol.a) {
var verified = await b.mail.bimi.fetchAndVerifyMark({
domain: "example.com",
vmcUrl: pol.a,
});
}
b.mail.bimi.validateTinyPsSvg(svgBytes) #
{
svgBytes: Buffer | string
}
Validates a brand-mark SVG against the AuthIndicators-WG Tiny PS profile (RFC 9091 5). Tiny-PS is a strict subset of SVG 1.2: single
var rv = b.mail.bimi.validateTinyPsSvg('');
// -> { ok: true, violations: [] }
b.mail.bimi.fetchAndVerifyMark(opts) #
{
{
domain: string, // required - BIMI domain to assert
// matches subjectAltName URI
vmcUrl: string?, // VMC PEM URL (https://); operator
// passes one of vmcUrl / cmcUrl
cmcUrl: string?, // CMC PEM URL (https://); same
trustAnchorsPem: string?, // operator-supplied PEM bundle;
// defaults to the vendored
// bimi-trust-anchors.pem
timeoutMs: number?, // default 15s
maxResponseBytes: number?, // default 256 KiB
audit: { safeEmit }, // operator-supplied audit dispatcher
httpClient: object?, // default b.httpClient - test-only
// override for unit tests that
// want to stub the network call
evidenceDocument: string?, // operator-supplied trademark
// evidence URL; surfaced on
// the result for audit logging
}
}
Fetches a VMC / CMC PEM from opts.vmcUrl (or opts.cmcUrl) over HTTPS, parses it as X.509, validates the chain against the BIMI Group trust anchors (vendored at lib/vendor/bimi-trust-anchors.pem, operator-overridable via trustAnchorsPem), confirms the cert's subjectAltName URI matches the BIMI domain, and confirms the cert carries the BIMI mark-verification ExtendedKeyUsage OID (1.3.6.1.5.5.7.3.31). Returns { ok, mark, certificate, vmcType } where vmcType is "vmc" or "cmc" derived from the cert's policyOIDs, and mark carries the SVG bytes when the cert's RFC 3709 logotype extension is present (or null when not). Throws MailBimiError with one of the documented codes on any failure.
var rv = await b.mail.bimi.fetchAndVerifyMark({
domain: "example.com",
vmcUrl: "https://example.com/bimi/cert.pem",
trustAnchorsPem: "-----BEGIN CERTIFICATE-----\n...",
});
// -> { ok, mark: { svg, evidenceDocument }, certificate, vmcType: "vmc" }
b.mail.crypto.pgp.sign(opts) #
{
audit:,
creationTime:,
message:,
passphrase:,
privateKeyPem:,
}
Produces a v4 OpenPGP detached signature over opts.message and returns the ASCII-armored signature plus a ready-to-emit multipart/signed; protocol="application/pgp-signature" body (RFC 3156 §5). Ed25519 (algorithm 22) and RSA-PKCS#1-v1.5 over SHA-256 (algorithm 1) are the v1 signing forms; RSA keys below 2048 bits are refused per RFC 8301 §3.1.
var rv = b.mail.crypto.pgp.sign({
message: "rfc822 body bytes",
privateKeyPem: pem,
});
// → { armored, multipartSigned, signedAt, fingerprint }
b.mail.crypto.pgp.verify(opts) #
{
armored:,
audit:,
message:,
publicKeyPem:,
}
Verifies an ASCII-armored OpenPGP detached signature against opts.message using opts.publicKeyPem. The signature's hash algorithm is enforced against the recomputed digest; SHA-1 is refused. Returns the v4 signer fingerprint (RFC 9580 §5.5.4) so callers can pin to a known operator key rather than trusting any key that happens to verify.
var rv = b.mail.crypto.pgp.verify({
message: bytes,
armored: "-----BEGIN PGP SIGNATURE----- ...",
publicKeyPem: pubPem,
});
// → { ok: true, signerFingerprint, signedAt, hashAlg }
b.mail.crypto.pgp.experimental.wkd.fetch(email, opts) #
{
httpsGet: Function, // (url) → Promise<{ status, body }>; REQUIRED
advancedHost: string, // passed through to computeUrl
maxKeyBytes: number, // default 256 KiB
}
Fetch a WKD key for email per draft-koch-openpgp-webkey-service. Tries the direct URL first; on 404 / network failure falls back to the advanced URL. opts.httpsGet(url) → Promise<{ status, body: Buffer }> is operator-supplied so the framework doesn't couple to a specific HTTP client. Returns { keyBytes, source: "direct" | "advanced", url } or throws mail-crypto/pgp/wkd-not-found when both URLs fail.
var key = await b.mail.crypto.pgp.experimental.wkd.fetch("alice@example.com", {
httpsGet: function (url) {
return b.httpClient.request({ url: url, method: "GET" });
},
});
b.mail.crypto.smime.sign(opts) #
{
message: Buffer|string, // message bytes to sign (signed-as-is)
certificate: Buffer, // DER-encoded signer cert
secretKey: Uint8Array, // PQC private key (b.pqcSoftware.ml_dsa_*.keygen())
sigAlg: "ML-DSA-65"|"ML-DSA-87"|"SLH-DSA-SHAKE-256f",
digestAlg: "sha3-256"|"sha3-512", // default sha3-512
boundary: string, // optional; auto-generated if omitted
audit: object, // optional b.audit handle
}
Sign an RFC 5322 message with S/MIME 4.0 (RFC 8551) producing a multipart/signed; protocol="application/pkcs7-signature" wrapper. The CMS SignedData payload is encoded via b.cms.encodeSignedData with PQC signers (ML-DSA-65 / ML-DSA-87 / SLH-DSA-SHAKE-256f). Returns { multipart, signature } where multipart is the wire representation (Content-Type + body) and signature is the raw CMS DER for operators that want to handle the MIME framing themselves.
var kp = b.pqcSoftware.ml_dsa_65.keygen();
var out = b.mail.crypto.smime.sign({
message: "From: x@y\r\nSubject: hi\r\n\r\nbody",
certificate: certDer,
secretKey: kp.secretKey,
sigAlg: "ML-DSA-65",
});
out.multipart; // → "Content-Type: multipart/signed; ..."
b.mail.crypto.smime.verify(opts) #
{
message: Buffer|string, // original signed bytes (use sign().multipart's first part)
signature: Buffer, // raw CMS DER (sign().signature)
signerPublicKey: Uint8Array, // PQC public key of the expected signer
audit: object,
}
Verify an RFC 8551 multipart/signed S/MIME envelope. Parses the CMS SignedData payload, recomputes the message digest, compares against the message-digest signed-attribute, and verifies the signature against the signer's PQC public key. Returns { valid, signerPublicKey, sigAlg, digestAlg } on success; throws on any mismatch.
var ok = b.mail.crypto.smime.verify({
message: msgBytes,
signature: cmsDer,
signerPublicKey: kp.publicKey,
});
ok.valid; // → true
b.mail.crypto.smime.verifyAll(opts) #
{
message: Buffer|string,
signature: Buffer,
signerPublicKeys: { [serialHex]: Uint8Array },
audit: object,
}
Multi-signer verify. The CMS SignedData can carry multiple SignerInfos; this routes each through verify() against the matching key in opts.signerPublicKeys (a map keyed by signer identifier serial-number-hex). Returns { valid, signers: [{ sid, sigAlg, digestAlg }] } where valid is true only when EVERY SignerInfo verified. Refuses with mail-crypto/smime/missing-key when a SignerInfo's sid has no operator-supplied public key.
var v = b.mail.crypto.smime.verifyAll({
message: msg,
signature: cmsDer,
signerPublicKeys: {
"01": signer1Pub,
"02": signer2Pub,
},
});
v.valid; // → true only when every signer verified
v.signers.length; // → 2
b.mail.crypto.smime.checkCert(opts) #
{
certPem:,
}
Operator-side cert preflight that lights up at boot: refuses SHA-1 / MD5 signatures, RSA keys < 2048 bits, MD2 / MD5 / SHA-1 as the certificate-signature algorithm. Returns the parsed cert shape: the full subject / issuer DN strings, the validity window, the signature algorithm (name + OID), the key type, and the SHA-256 fingerprint. Throws mail-crypto/smime/bad-cert on any of the above; throws mail-crypto/smime/expired-cert if the cert is outside its validity window.
var info = b.mail.crypto.smime.checkCert({ certPem: pem });
// → { subject, issuer, validFrom, validTo, sigAlgName, sigAlgOid, keyType, fingerprint256 }
b.mail.crypto.isMailCryptoError(err) #
Duck-type check that returns true for any MailCryptoError raised by either sub-namespace. Each sub-module defines its own MailCryptoError class so instanceof doesn't span them; this helper checks the isMailCryptoError === true flag both classes set, giving operators one cross-protocol catch-all.
try {
b.mail.crypto.pgp.verify(opts);
} catch (e) {
if (b.mail.crypto.isMailCryptoError(e)) { handle(e); }
}
b.mail.dav.create(opts) #
{
storage: { calendar, addressbook }, // operator-supplied
profile: "strict" | "balanced" | "permissive", // default strict
compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2", // optional
maxRequestBodyBytes: number, // default 8 MiB
audit: b.audit, // optional
}
Build a CalDAV + CardDAV route-handler bundle. Returns a handle exposing caldavHandler / carddavHandler / discoveryHandler (Express-style (req, res, next) functions) plus dispatchCaldav / dispatchCarddav for operators on a non-Express transport.
var dav = b.mail.dav.create({
storage: {
calendar: { listCalendars, getComponent, listComponents,
putComponent, deleteComponent, mkcalendar },
addressbook: { listAddressbooks, getCard, listCards,
putCard, deleteCard, mkcol },
},
profile: "strict",
});
app.use("/.well-known/caldav", dav.discoveryHandler);
app.use("/.well-known/carddav", dav.discoveryHandler);
app.use("/caldav", bearerAuth, dav.caldavHandler);
app.use("/carddav", bearerAuth, dav.carddavHandler);
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 });
},
}));
b.mail.dkim.bootstrap(opts) #
{
domain: string, // required — RFC 5321 domain
selector: string, // required — RFC 6376 §3.1 selector (the `s1` in s1._domainkey.example.com)
algorithm: "ed25519-sha256" | "rsa-sha256" | "dual",
// default: "ed25519-sha256"
rsaBits: number, // RSA-only; default 2048; refused below 1024 (RFC 8301 §3.1)
rsaSelector: string, // dual-only; selector for the RSA key (defaults to selector + "-rsa")
}
Bootstrap a DKIM keypair + DNS TXT record + ready-to-use signer. Operators deploying outbound mail (b.mail.send, b.mail.server.submission) need three things in place: (1) a private signing key, (2) the matching public key published as a DNS TXT record under , (3) a b.mail.dkim.create(...) handle wired into the outbound agent. Pre-this-primitive every consumer reinvented the keypair-mint + DNS-record-serialize plumbing; this primitive owns it.
Default algorithm is ed25519-sha256 (RFC 8463): smaller DNS record, faster signing, modern crypto. Operators with receivers that don't yet support Ed25519 pass algorithm: "rsa-sha256" for RFC 6376 (defaults to 2048-bit RSA per RFC 8301 §3.1 guidance — opt up with rsaBits). Passing algorithm: "dual" mints BOTH keypairs and returns a b.mail.dkim.dualSigner-shaped signer that emits two DKIM-Signature headers (one per alg) for max receiver compat per RFC 8463 §3 dual- signing pattern.
var dkim = b.mail.dkim.bootstrap({ domain: "example.com", selector: "s1" });
// → {
// algorithm: "ed25519-sha256",
// domain: "example.com",
// selector: "s1",
// privateKeyPem,
// publicKeyPem,
// dnsName: "s1._domainkey.example.com",
// dnsTxtValue: "v=DKIM1; k=ed25519; p=MCowBQYDK2Vw...",
// dnsRecord: 's1._domainkey.example.com. IN TXT ("v=DKIM1; k=ed25519; p=MCo...")',
// signer: fn(headersToSign?, canonicalization?) → signer,
// }
// Operator seals the private key via the vault then wires the signer:
var sealedPath = b.vault.sealPemFile({ source: "/var/lib/blamejs/dkim.key", destination: "/var/lib/blamejs/dkim.key.sealed" });
var signer = dkim.signer(); // uses dkim.privateKeyPem in-memory
// Dual signing — RSA + Ed25519 for max receiver compatibility:
var dkim2 = b.mail.dkim.bootstrap({ domain: "example.com", selector: "s1", algorithm: "dual" });
// dkim2.signer() returns a dualSigner emitting both DKIM-Signature headers.
b.mail.greylist.create(opts?) #
{
profile: "strict" | "balanced" | "permissive",
posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
store: { get, put, delete, gc } — pluggable backend
minDelayMs: number — overrides profile minimum-delay window
whitelistTtlMs: number — overrides profile post-acceptance TTL
maxEntries: number — in-memory backend's entry cap
allowedSources: Array — IPs / CIDRs that skip greylisting
audit: b.audit namespace
}
Build a greylist instance. Returns an object with .check(ctx) → Promise and .gc({ olderThanMs }) → Promise<{ removed }>.
var gl = b.mail.greylist.create({ profile: "strict" });
var v = await gl.check({
ip: "203.0.113.42",
mailFrom: "sender@example.com",
rcptTo: "alice@operator.example",
});
if (v.action === "defer") return reply(451, "4.7.1 " + v.reason);
b.mail.greylist.compliancePosture(posture) #
Return the effective profile name for a compliance posture, or null for unknown posture names.
b.mail.greylist.compliancePosture("hipaa"); // → "strict"
b.mail.helo.evaluate(ctx, opts?) #
{
profile: "strict" | "balanced" | "permissive",
posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
selfNames: string[], // operator's MX hostnames; claim of these by a peer refused
genericRdnsPatterns: RegExp[], // additional patterns layered onto built-ins
fcrdnsRequiredFor: ("v4" | "v6")[], // overrides profile's FCrDNS list
audit: b.audit namespace,
}
Evaluate a peer's HELO / EHLO identity claim. Returns a verdict shape the MX listener consumes to drive accept / reject / score-tag policy.
var resolver = b.network.dns.resolver.create();
var v = await b.mail.helo.evaluate({
ip: "203.0.113.42",
claimedName: "mail.example.com",
resolver: resolver,
}, { profile: "strict" });
if (v.action === "reject-shape") return reply(550, v.reason);
b.mail.helo.compliancePosture(posture) #
Return the effective profile name for a compliance posture, or null for unknown posture names.
b.mail.helo.compliancePosture("hipaa"); // → "strict"
b.mail.journal.create(opts) #
{
storage: b.objectStore.bucketOps handle,
regimes: string[],
vault: b.vault handle,
legalHold: b.legalHold handle,
db: b.db handle,
audit: b.audit namespace,
namespace: string,
}
Returns a journal handle bound to the operator-supplied WORM bucket. The bucket SHOULD have Object Lock / immutability enabled at the storage layer (S3 ObjectLockEnabled, Azure Immutable Blob, GCS retention-policy) — the journal primitive emits an audit warning at create-time if the bucket reports objectLockEnabled: false, but doesn't refuse since some operator deployments use FS-level WORM via filesystem ACLs the framework can't introspect.
var journal = b.mail.journal.create({
storage: operatorWormBucket,
regimes: ["sec-17a-4", "finra-4511"],
vault: b.vault,
legalHold: b.legalHold,
db: b.db,
});
await journal.record({
direction: "inbound",
actorId: "compliance",
messageId: "",
headers: { from: "alice@x.com", to: "bob@y.com", subject: "Q3 results" },
bodyBytes: rfc822Bytes,
envelope: { mailFrom: "alice@x.com", rcptTo: ["bob@y.com"] },
});
b.mail.rbl.create(opts) #
{
resolver: b.network.dns.resolver.create() instance, required
blocklists: Array — DNS zones (e.g. "bl.spamcop.net")
allowlists: Array — DNSWL zones (e.g. "list.dnswl.org")
profile: "strict" | "balanced" | "permissive"
posture: "hipaa" | "pci-dss" | "gdpr" | "soc2"
withReason: boolean — default false; fetch TXT record per A hit
audit: b.audit namespace
}
Build an RBL query instance. Returns an object with .query(ip, opts) → Promise and .queryDomain(domain, opts) → Promise methods.
var rbl = b.mail.rbl.create({
resolver: b.network.dns.resolver.create(),
blocklists: ["zen.spamhaus.org", "bl.spamcop.net"],
});
var verdict = await rbl.query("192.0.2.99", { withReason: true });
if (verdict.listed.length) refuseConnection(verdict.listed[0].reason);
b.mail.rbl.reverseIp(ip) #
Build the reverse-DNS query name for an IPv4 or IPv6 address per RFC 5782 §2.1 / §2.4. Pure-functional helper exposed for operator tests and the b.mail.dnsbl extension primitive.
b.mail.rbl.reverseIp("192.0.2.99"); // → "99.2.0.192"
b.mail.rbl.reverseIp("2001:db8::1"); // → "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2"
b.mail.rbl.compliancePosture(posture) #
Return the effective profile name for a compliance posture, or null for unknown posture names.
b.mail.rbl.compliancePosture("hipaa"); // → "strict"
b.mail.requireTls.peerSupports(ehloLines) #
Walk a parsed EHLO response and return true when the peer advertised the REQUIRETLS keyword. ehloLines is the array of post-greeting capability lines returned by the SMTP transport (each entry is the capability token, e.g. "SIZE 10485760", "PIPELINING", "REQUIRETLS"). Case-insensitive match per RFC 5321 §2.4 (EHLO keywords are uppercase by convention but comparison is case-insensitive).
Returns false for empty / non-array input — operators who can't parse the EHLO get a definitive "not supported" verdict rather than a throw, matching the "defensive request-shape reader" convention used elsewhere.
var ehlo = ["mail.example.com", "PIPELINING", "SIZE 10485760", "REQUIRETLS", "STARTTLS"];
b.mail.requireTls.peerSupports(ehlo); // → true
b.mail.requireTls.peerSupports(["PIPELINING", "SIZE 10485760"]); // → false
b.mail.requireTls.mailFromExtension(opts) #
{
requireTls: boolean, // true to emit " REQUIRETLS"; falsy/absent → ""
}
Build the trailing SMTP MAIL FROM extension token for REQUIRETLS. Returns " REQUIRETLS" (with a leading space, ready to append) when opts.requireTls === true; empty string otherwise. The primitive does NOT validate the operator's address — that's the SMTP transport's job. This only emits the standard-defined token suffix.
Refuses non-object opts. requireTls must be a boolean when provided (any other type throws mail-require-tls/bad-flag) so a truthy-but-wrong-shape value (e.g. "yes") doesn't silently succeed.
var line = "MAIL FROM:" +
b.mail.requireTls.mailFromExtension({ requireTls: true });
// → "MAIL FROM: REQUIRETLS"
b.mail.requireTls.parseTlsRequiredHeader(headerValue) #
Parse the RFC 8689 §5 TLS-Required header field. Returns:
- "no" when the value is the literal token no (case- insensitive, ignoring surrounding whitespace) — the sender EXPLICITLY opts out of REQUIRETLS-style behavior for this message. - "yes" for any other non-empty value — conservative default so an operator who set a typo / malformed value still gets the strict path (RFC 8689 §5: "if a recipient receives a message containing a TLS-Required field with any value other than 'No', it MUST be treated as if the field had been absent"). - null when the header is absent / empty / not a string — operator code branches on null vs "yes" / "no".
Refuses CR / LF / NUL in the value (header-injection-shape inputs shouldn't reach a parser that's downstream of header splitters anyway, but a defensive check here catches operator-side mistakes).
b.mail.requireTls.parseTlsRequiredHeader("No"); // → "no"
b.mail.requireTls.parseTlsRequiredHeader("no"); // → "no"
b.mail.requireTls.parseTlsRequiredHeader(" no "); // → "no"
b.mail.requireTls.parseTlsRequiredHeader("yes"); // → "yes"
b.mail.requireTls.parseTlsRequiredHeader("anything"); // → "yes" (RFC 8689 §5 default)
b.mail.requireTls.parseTlsRequiredHeader(""); // → null
b.mail.requireTls.parseTlsRequiredHeader(undefined); // → null
b.mail.scan.create(opts) #
{
host: string — required. ICAP / clamd hostname or IP.
port: number — required. ICAP port (default 1344) /
clamd port (default 3310).
service: string — ICAP service name (default "srv_clamav").
protocol: "icap" | "clamav-instream" — default "icap".
timeoutMs: number — per-request wall clock; default per profile.
profile: "strict" | "balanced" | "permissive".
posture: "hipaa" | "pci-dss" | "gdpr" | "soc2".
audit: b.audit instance (drop-silent on failure).
}
Build a mail-scan handle. Returns { scan(messageBytes, opts), profile, protocol, MailScanError } where .scan resolves to { verdict, icapResponse?, threats?, durationMs }:
- verdict: "clean" | "infected" | "error". - icapResponse: the structured b.safeIcap.parse result on ICAP backend (omitted on clamav-instream). - threats: ArraydurationMs: round-trip ms (audit / metrics).
var scanner = b.mail.scan.create({
host: "av.internal",
port: 1344,
service: "srv_clamav",
});
var verdict = await scanner.scan(rawMessage);
if (verdict.verdict === "infected") refuseMessage(verdict.threats);
b.mail.scan.compliancePosture(posture) #
Return the effective profile name for a compliance posture, or null for unknown posture names.
b.mail.scan.compliancePosture("hipaa"); // → "strict"
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
policy: {
mtaSts: "enforce" | "testing" | "off", // default "enforce" — RFC 8461 posture
dane: "opportunistic" | "enforce" | "off", // default "opportunistic" — RFC 7672
},
retry: {
maxAttempts: number, // default 5
backoffMs: Array, // default [1m, 5m, 15m, 1h, 4h]
},
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.smtpTransport, 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.
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)
b.mail.server.imap.create(opts) #
{
tlsContext: SecureContext, // required (no plaintext mode)
greeting: string, // default "blamejs IMAP4rev2"
maxLineBytes: number, // default 8192
maxLiteralBytes: number, // default 64 MiB
idleTimeoutMs: number, // default 30 min
profile: "strict" | "balanced" | "permissive",
auth: {
mechanisms: ["PLAIN", "LOGIN", "SCRAM-SHA-256", "EXTERNAL", "XOAUTH2"],
verify: async function (mechanism, credentials) → { ok, actor },
},
mailStore: b.mailStore handle, // required
rateLimit: b.mail.server.rateLimit handle | opts | false,
audit: b.audit // optional
}
Build an IMAP4rev2 listener (RFC 9051). The handle exposes listen({ port, address }) → ephemeral-bind promise resolving to { port, address }, plus close() for graceful shutdown.
var imap = b.mail.server.imap.create({
tlsContext: b.mail.server.tls.context({ certFile, keyFile }).secureContext,
auth: {
mechanisms: ["PLAIN", "SCRAM-SHA-256"],
verify: async function (mech, creds) {
return { ok: true, actor: { tenantId: "t1", username: creds.authzid } };
},
},
mailStore: b.mailStore.create({ backend: b.db.handle() }),
});
await imap.listen({ port: 143 });
b.mail.server.jmap.create(opts) #
{
mailStore: b.mailStore handle (operator-supplied backend),
methods: { "/": async fn(actor, args, ctx) },
// operator-supplied JMAP method handlers
serverCapabilities: { "": },
// capabilities the server advertises beyond core
accountsFor: async function (actor) → { primaryAccounts, accounts },
// operator-supplied accountId enumeration
profile: "strict" | "balanced" | "permissive",
posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
audit: b.audit // optional
}
Build a JMAP Core + JMAP Mail listener. Returns a handle exposing apiHandler / sessionHandler / discoveryHandler (Express-style (req, res, next) functions) and dispatch(actor, body) for operators with a non-Express transport.
var jmap = b.mail.server.jmap.create({
mailStore: b.mailStore.create({ backend: b.db.handle() }),
methods: {
"Mailbox/get": async function (actor, args) {
return { accountId: args.accountId, list: [], notFound: [] };
},
},
serverCapabilities: { "urn:ietf:params:jmap:mail": {} },
accountsFor: async function (actor) {
return {
primaryAccounts: { "urn:ietf:params:jmap:mail": "A1" },
accounts: { A1: { name: actor.username } },
};
},
});
app.post("/jmap/api", b.middleware.bearerAuth({ verify: verify }), jmap.apiHandler);
b.mail.server.jmap.emailSubmissionSetHandler(opts) #
{
deliver: async function (envelope), // b.mail.send.deliver instance (REQUIRED)
lookupEmail: async function (emailId, accountId, actor) → Buffer|null, (REQUIRED)
identities: function (accountId) → [ { id, email, mayDelegate } ], (REQUIRED)
onCreated: async function (subId, submission, accountId), (optional)
onDestroyed: async function (subId, accountId), (optional)
onCancel: async function (subId, accountId) → boolean, (optional — undo support)
maxRecipients: number, // default 1000
}
Reference implementation of JMAP EmailSubmission/set (RFC 8621 §7.5) that composes b.mail.send.deliver. Returns an async method-handler suitable for plumbing into b.mail.server.jmap.create({ methods: ... }).
The handler:
1. Walks args.create per RFC 8621 §7.5. For each EmailSubmission: - Refuses identityId not registered in opts.identities(accountId). - Refuses emailId absent — calls opts.lookupEmail(emailId, accountId, actor) to fetch the RFC 822 blob (refuses emailNotFound when null). - Refuses missing or oversize envelope.rcptTo (max 1000 per the same recipient cap b.mail.send.deliver enforces). - Validates envelope.mailFrom.email matches the identity's authorized addresses (forbiddenMailFrom per RFC 8621 §7.5.1.2 when not). 2. Hands the RFC 822 blob to the supplied opts.deliver(envelope) (a b.mail.send.deliver.create() instance). 3. Maps deliver's { delivered, deferred, failed } result into JMAP deliveryStatus (recipient → { smtpReply, delivered, displayed } per RFC 8621 §7.4). 4. Calls opts.onCreated(subId, submission, accountId) so the operator can persist the EmailSubmission record (state survives across JMAP requests via EmailSubmission/get).
args.destroy removes EmailSubmission records via opts.onDestroyed(subId, accountId) — the delivery itself cannot be unsent at this point; destroy only removes the JMAP-visible record.
args.update is honored only for the undoStatus: "canceled" transition per RFC 8621 §7.5.2 (operators with a queue-based deferred-send model wire opts.onCancel(subId, accountId); the reference handler refuses with cannotUnsend when no onCancel is configured).
var deliver = b.mail.send.deliver({ hostname: "mta.example.com" });
var emailSubSet = b.mail.server.jmap.emailSubmissionSetHandler({
deliver: deliver,
lookupEmail: async function (emailId, accountId) {
return mailStore.fetchBlob(accountId, emailId);
},
identities: function (accountId) {
return [{ id: "I1", email: "ops@example.com" }];
},
onCreated: async function (id, sub, accountId) { return; },
});
var jmap = b.mail.server.jmap.create({
mailStore: store,
accountsFor: async function () { return { primaryAccounts: {}, accounts: {} }; },
methods: { "EmailSubmission/set": emailSubSet },
});
b.mail.server.managesieve.create(opts) #
{
tlsContext: SecureContext, // required (no implicit plaintext)
allowPlaintext: boolean, // explicit opt-in; emits warning audit
greeting: string, // default "blamejs ManageSieve"
maxLineBytes: number, // default 8192
idleTimeoutMs: number, // default 5 min
profile: "strict" | "balanced" | "permissive", // default "strict"
auth: {
mechanisms: ["SCRAM-SHA-256", "OAUTHBEARER", ...], // SASL mechs to advertise
verify: async function (mech, credentials) → { ok, actor },
},
mailStore: b.mailStore handle, // must expose sieveScripts.*
rateLimit: b.mail.server.rateLimit handle | opts | false,
audit: b.audit
}
Build a ManageSieve listener (RFC 5804). Returns a handle exposing listen({ port, address }) and close(). Composes b.safeSieve for PUTSCRIPT pre-validation per RFC 5804 §2.3.
var msv = b.mail.server.managesieve.create({
tlsContext: b.mail.server.tls.context({ certFile, keyFile }).secureContext,
auth: {
mechanisms: ["SCRAM-SHA-256", "OAUTHBEARER", "EXTERNAL"],
verify: async function (mech, creds) {
return { ok: true, actor: { username: creds.authzid, tenantId: "t1" } };
},
},
mailStore: b.mailStore.create({ backend: b.db.handle() }),
});
await msv.listen({ port: 4190 });
b.mail.server.mx.create(opts) #
{
tlsContext: TlsContext, // required — b.network.tls.context() output (no implicit plaintext)
greeting: string, // default "blamejs ESMTP" — HELO/EHLO 220-line banner
helo: b.mail.helo, // optional gate — HELO identity (FCrDNS / shape / self-name)
rbl: b.mail.rbl.create(…), // optional gate — DNS blocklist on the connecting IP
greylist: b.mail.greylist.create(…), // optional gate — defer first-seen (ip, from, rcpt)
agent: b.mail.agent, // optional delivery handoff
relayAllowedFor: [{ cidr, scope }], // operator-explicit relay allowlist; default [] = MX-only
localDomains: [string], // RCPT TO local-domain allowlist (refuse non-local with 550 5.7.1)
maxLineBytes: number, // default 1 KiB — per-command line cap
maxMessageBytes: number, // default 50 MiB — DATA body cap
maxRcptsPerMessage: number, // default 100 — per RFC 5321 §4.5.3.1.8
idleTimeoutMs: number, // default 5 minutes — RFC 5321 §4.5.3.2.7
profile: "strict" | "balanced" | "permissive", // gate posture cascade
guardEnvelope: true | { // optional gate — DATA-phase SPF/DKIM/DMARC via b.mail.inbound.verify
mode?: "enforce" | "monitor", // default: enforce (monitor when profile is permissive)
onTemperror?: "defer" | "accept", // DNS temperror disposition; default "defer" (451 4.7.5)
authservId?: string, // RFC 8601 authserv-id; default localDomains[0]
dnsLookup?: function, // async (qname, type) override for SPF/DKIM/DMARC lookups
maxSignatures?: number, // DKIM verify cap (1-16)
clockSkewMs?: number, // DKIM timestamp skew tolerance
minRsaBits?: number, // DKIM minimum RSA key size
timeoutMs?: number, // pipeline wall-clock ceiling; default 20s (timeout → temperror disposition)
},
}
Build the MX listener. Returns { listen({ port?, address? }), close({ timeoutMs? }), connectionCount(), _portForTest() }.
var tls = b.network.tls.context({ cert: certPem, key: keyPem });
var server = b.mail.server.mx.create({
tlsContext: tls,
greeting: "mx.example.com ESMTP blamejs",
helo: b.mail.helo,
rbl: b.mail.rbl.create({ providers: ["zen.spamhaus.org"] }),
greylist: b.mail.greylist.create({ store: greylistStore }),
agent: b.mail.agent.create({ store: mailStore }),
localDomains: ["example.com"],
});
await server.listen({ port: 25 });
b.mail.server.pop3.create(opts) #
{
tlsContext: SecureContext, // required (no plaintext)
greeting: string, // default "blamejs POP3"
maxLineBytes: number, // default 1024
idleTimeoutMs: number, // default 10 min
commitTimeoutMs: number, // default 30 s (UPDATE-state mailStore.commitPop3Drop cap)
profile: "strict" | "balanced" | "permissive",
auth: {
mechanisms: ["PLAIN"], // SASL mechs to advertise
verify: async function (mech, credentials) → { ok, actor },
},
mailStore: b.mailStore handle,
rateLimit: b.mail.server.rateLimit handle | opts | false,
audit: b.audit
}
Build a POP3 listener (RFC 1939). Returns a handle exposing listen({ port, address }) and close(). POP3 is opt-in legacy — deployments should prefer b.mail.server.imap + b.mail.server.jmap for new MUAs.
var pop3 = b.mail.server.pop3.create({
tlsContext: b.mail.server.tls.context({ certFile, keyFile }).secureContext,
auth: {
mechanisms: ["PLAIN"],
verify: async function (mech, creds) {
return { ok: true, actor: { username: creds.authzid, tenantId: "t1" } };
},
},
mailStore: b.mailStore.create({ backend: b.db.handle() }),
});
await pop3.listen({ port: 110 });
b.mail.server.rateLimit.create(opts?) #
{
maxConcurrentConnectionsPerIp: number, // default 10
connectionsPerIpPerMinute: number, // default 60
authFailuresPerIpPer15Min: number, // default 10
minBytesPerSecond: number, // default 100 (DATA-body slow-loris floor)
rcptFailuresPerIpPerMinute: number, // default 50 (RCPT 550 enumeration bound)
disabled: boolean, // default false — test escape hatch
}
Build a rate-limit handle. The listeners compose this internally with the framework defaults; operators override caps by passing their own rateLimit opt to b.mail.server.mx.create or b.mail.server.submission.create. Direct construction is for operators sharing one budget across multiple listeners (e.g. an MX + a submission server on the same IP space).
var rl = b.mail.server.rateLimit.create({
maxConcurrentConnectionsPerIp: 5,
connectionsPerIpPerMinute: 30,
});
var ok = rl.admitConnection("192.0.2.1");
// → { ok: true } or { ok: false, reason: "concurrent-per-ip" | "rate-per-ip" }
b.mail.server.rateLimit.resolve(spec) #
Resolve a rate-limit spec into a limiter. false disables limiting (a disabled limiter that always admits), an already-built limiter — one exposing admitConnection — passes through unchanged, and anything else is treated as create() options. Every mail server (IMAP / POP3 / SMTP MX / Submission / ManageSieve) composes this at the top of its create() so the spec contract is identical across protocols.
var b = require("blamejs");
var rl = b.mail.server.rateLimit.resolve(false); // disabled
// → a limiter whose admitConnection always admits
b.mail.serverRegistry.create(opts) #
{
protocol: "imap" | "jmap" | "managesieve",
defaults: { [name]: { fn, maxHandlerBytes, maxHandlerMs } },
overrides: { [name]: { fn, maxHandlerBytes, maxHandlerMs } },
notFoundHandler: function (name, ctx), // optional; returns the protocol's "not configured" reply
}
Build a per-method dispatch registry for one of the mail-server listeners. Returns { register, unregister, dispatch, list, has, source, MailServerRegistryError }.
var reg = b.mail.serverRegistry.create({
protocol: "imap",
defaults: { CAPABILITY: { fn: _capabilityHandler,
maxHandlerBytes: 8 * 1024,
maxHandlerMs: 5 * 1000 } },
overrides: opts.overrides || {},
});
await reg.dispatch("CAPABILITY", state, socket, parsed);
b.mail.server.submission.create(opts) #
{
tlsContext: TlsContext, // required — b.network.tls.context() output
implicitTls: boolean, // wrap connection in TLS from the SYN (port 465); default false
greeting: string, // EHLO/220 banner; default "blamejs Submission"
auth: object, // SASL config (required unless permissive profile)
mechanisms: string[], // SASL mechs to advertise; default ["PLAIN","LOGIN"]
verify: function, // async (mechanism, credentials) => { ok, actor }
rateLimit: object, // optional b.middleware.rateLimit instance for failure budget
agent: object, // outbound delivery handoff (handoff({ ... }) → ack)
identityBinding: "strict" | "permissive", // MAIL FROM must match auth identity (default strict)
maxLineBytes: number, // default 1 KiB
maxMessageBytes: number, // default 50 MiB
maxRcptsPerMessage: number, // default 100
idleTimeoutMs: number, // default 5 minutes
profile: string, // "strict" | "balanced" | "permissive"; default "strict"
}
Build the submission listener. Returns { listen({ port?, address? }), close({ timeoutMs? }), connectionCount(), _portForTest() }.
var tls = b.network.tls.context({ cert: certPem, key: keyPem });
var server = b.mail.server.submission.create({
tlsContext: tls,
greeting: "smtp.example.com Submission blamejs",
auth: {
mechanisms: ["PLAIN", "SCRAM-SHA-256"],
verify: async function (mech, creds) {
var actor = await myAuthService.verify(mech, creds);
return actor ? { ok: true, actor: actor } : { ok: false };
},
},
agent: b.mail.agent.create({ outboundSend: b.mail.send }),
});
await server.listen({ port: 587 });
b.mail.server.tls.context(opts) #
{
certFile: string, // required — PEM-encoded fullchain
keyFile: string, // required — PEM-encoded private key (raw OR sealed)
vault: object, // optional — b.vault; when supplied + keyFile
// starts with the b.vault.sealPemFile magic
// ("vault:"), unsealed before use
watch: boolean, // default false — when true, poll for rotation
pollMs: number, // default 30000; min 1000
}
Build a node:tls SecureContext from cert + key PEM file paths. Returns a handle exposing secureContext, reload(), onReload(fn), and stop(). When watch: true, the helper polls both files for mtime changes (default every 30s) and rebuilds the context in-place on change — operators wire onReload to swap the running listener's context after cert rotation.
var tls = b.mail.server.tls.context({
certFile: "/etc/letsencrypt/live/mail.example.com/fullchain.pem",
keyFile: "/etc/letsencrypt/live/mail.example.com/privkey.pem",
watch: true,
});
// Wire `tls.secureContext` into b.mail.server.mx.create / submission.create
tls.onReload(function (newCtx) {
// operator swaps the running listener's SecureContext via the
// listener's reload hook (when the listener exposes one) or via
// restart-on-rotation flow
});
// ... later, on shutdown:
tls.stop(); // clears the poll timer
b.mail.server.tls.upgradeSocket(opts) #
{
plainSocket: net.Socket, // pre-upgrade socket
secureContext: tls.SecureContext, // from b.mail.server.tls.context
idleTimeoutMs: number, // re-armed post-handshake
onSecure: function(tlsSocket), // called once "secure" fires
onData: function(tlsSocket, chunk), // post-handshake ingest
onError: function(err), // handshake / runtime error
onTimeout: function(tlsSocket), // optional idle timeout cb
}
STARTTLS / STLS upgrade primitive shared by every mail-protocol listener (MX / submission / IMAP / POP3). Wraps the four-step dance every listener was inlining and that has been a recurring source of cleartext-injection bugs (CVE-2021-33515 Dovecot, CVE-2021-38371 Exim) when even one of the four steps is forgotten:
1. Remove ALL "data" listeners from the plain socket so any bytes the peer queued in the TCP receive buffer before the handshake do NOT reach the plaintext state machine after the socket has been re-typed as a TLSSocket. Without listener removal, plain-mode bytes pipelined ahead of the handshake reach the post-TLS dispatcher and execute under the authenticated TLS context. 2. Pause the plain socket so no further bytes flow through the old handler in the window before the TLSSocket attaches. 3. Re-arm the idle timeout on the new TLSSocket (the plain socket's setTimeout does not survive the upgrade — RFC 5321 §4.5.3.2.7 idle timeouts must keep running post-handshake). 4. Wire "secure" / "data" / "error" handlers via callbacks so the caller's per-protocol state machine keeps owning the ingest logic.
b.mail.server.tls.upgradeSocket({
plainSocket: socket,
secureContext: opts.tlsContext,
idleTimeoutMs: idleTimeoutMs,
onSecure: function (tlsSocket) { state.tls = true; },
onData: function (tlsSocket, chunk) { _ingest(state, tlsSocket, chunk); },
onError: function (err) { _emit("tls.handshake_failed", { err: err.message }); },
});
b.mail.server.tls.upgradeLineProtocol(opts) #
{
state: object, // connection state ({ lineBuffer, tls, … })
socket: net.Socket, // pre-upgrade plain socket
secureContext: tls.SecureContext, // from b.mail.server.tls.context
idleTimeoutMs: number, // re-armed post-handshake
clearFields: Array, // extra state fields to null pre-upgrade
drain: function(state, tlsSocket), // the protocol line drainer
onSecure: function(tlsSocket), // optional post-secure work
onError: function(err), // handshake / runtime error
onTimeout: function(tlsSocket), // optional idle-timeout handler
}
STARTTLS / STLS completion for the line-buffered store listeners (IMAP / POP3 / ManageSieve), layered over upgradeSocket. The caller has already validated protocol state and written its "begin TLS" response; this owns the steps that recur identically across the three:
1. Drop the pre-handshake state.lineBuffer (always) plus any protocol-specific half-parsed command / literal / auth fields (clearFields) so bytes the peer pipelined before the upgrade cannot survive into the post-TLS session (CVE-2021-33515 / CVE-2021-38371 STARTTLS-injection class). Centralizing the lineBuffer reset makes it impossible for a listener to forget. 2. Mark state.tls = true on the secure event, then run the caller's optional onSecure for protocol-specific work (e.g. ManageSieve re-emitting its capability banner per RFC 5804). 3. Feed every post-handshake chunk through the caller's drain via the standard state.lineBuffer append.
The transfer listeners (MX / submission) ingest via a serialized feed pump, not the line buffer, so they call upgradeSocket directly.
b.mail.server.tls.upgradeLineProtocol({
state: state, socket: socket, secureContext: opts.tlsContext,
idleTimeoutMs: idleTimeoutMs, clearFields: ["pendingLiteral"],
drain: _drainBuffer,
onError: function (err) { _emit("imap.tls_failed", { err: err.message }); _close(socket, state); },
});
b.mail.sieve.run(ast, env, opts?) #
{
maxGas: number, // default 10000; cap 1_000_000
}
Walk a parsed Sieve AST against the message environment + return the ordered action list. The interpreter is pure — it reads only from env and never mutates it; every action surfaces as an entry in the returned list for the caller to dispatch.
var ast = b.safeSieve.parse('if header :contains "X-Spam" "yes" { fileinto "Junk"; }');
var rv = b.mail.sieve.run(ast, {
headers: [{ name: "X-Spam", value: "yes" }],
envelope: { from: "sender@example.com", to: "rcpt@example.com" },
sizeBytes: 1024,
});
// → { actions: [{ kind: "fileinto", folder: "Junk" }, { kind: "keep" }], gas: 3, stopped: false }
b.mail.sieve.runScript(script, env, opts?) #
{
profile: "strict" | "balanced" | "permissive",
compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
maxGas: number,
}
Parse + run in one call. Most call sites — JMAP SieveScript/validate, MX delivery hook — want this shape.
var rv = b.mail.sieve.runScript(
'require ["fileinto"];\nif header :is "From" "boss@x.com" { fileinto "Important"; }',
{ headers: [{ name: "From", value: "boss@x.com" }] }
);
rv.actions[0].folder; // → "Important"
b.mail.sieve.create(opts?) #
{
maxGas: number,
profile: "strict" | "balanced" | "permissive",
compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
audit: { safeEmit: function },
}
Returns a stateful Sieve handle the delivery agent + JMAP SieveScript/validate method compose. Distinct from the bare b.mail.sieve.run(ast, env) entry — the handle carries operator- supplied opts (maxGas, profile, compliancePosture, audit) so every invocation runs with the same posture.
var sieve = b.mail.sieve.create({ profile: "strict", audit: b.audit });
sieve.validateScript(operatorScript);
var rv = await sieve.runScript(operatorScript, mailEnv);
b.mail.spamScore.create(opts) #
{
scorer: async fn({ rawBytes, headers, envelope }) → { score, reasons } — required
threshold: number — overrides profile default
profile: "strict" | "balanced" | "permissive"
posture: "hipaa" | "pci-dss" | "gdpr" | "soc2"
audit: b.audit instance
}
Build a spam-score handle. Returns { score(message, opts), threshold, profile, MailSpamScoreError } where .score resolves to { score, reasons, verdict }. verdict is "accept" / "score-tag" / "refuse" based on threshold comparison.
var spam = b.mail.spamScore.create({
scorer: async function (ctx) {
return await callSpamAssassin(ctx.rawBytes);
},
});
var v = await spam.score({ rawBytes: msg });
if (v.verdict === "refuse") refuseConnection(v.reasons.join(","));
b.mail.spamScore.compliancePosture(posture) #
Return the effective profile name for a compliance posture, or null for unknown posture names.
b.mail.spamScore.compliancePosture("hipaa"); // → "strict"
b.mail.srs.create(opts) #
{
secret: string, // operator's HMAC-SHA-256 signing secret (>=32 bytes recommended)
forwarderDomain: string, // the forwarder's own domain (where bounces land)
expiryDays: number, // default 30 — reject reverse() of rewrites older than this
}
Build an SRS rewriter bound to the operator's forwarder domain + HMAC signing secret. Returns { rewrite, srs1Rewrite, reverse } — rewrite produces an SRS0 origin address, srs1Rewrite chains an already-SRS0/SRS1 address as SRS1 for a further forwarding hop, and reverse decodes either form (SRS0 → original sender with HMAC + expiry checks; SRS1 → the prior forwarder's address, one hop back).
var srs = b.mail.srs.create({
secret: b.crypto.generateToken(64),
forwarderDomain: "forwarder.example",
});
// Inbound: alice@bob.com → forwarder → carol@dest.com
var rewritten = srs.rewrite("alice@bob.com");
// → "SRS0=HHHH=TT=bob.com=alice@forwarder.example"
// Bounce arrives back at SRS0=...; decode to deliver
var original = srs.reverse(rewritten);
// → "alice@bob.com"
// A further forwarding hop chains the already-SRS0 address as SRS1
var hop2 = srs.srs1Rewrite(rewritten);
// → "SRS1=HHHH=forwarder.example==HHHH=TT=bob.com=alice@forwarder.example"
srs.reverse(hop2); // → the prior-hop SRS0 address, re-routed one hop back
b.mail.toAscii(domain) #
RFC 3492 Punycode encode an IDN domain to its ASCII-compatible form. domain MUST be the part after @ — pass the local part separately. Returns the encoded ASCII string, or null when the input isn't a valid IDN-encodable domain. Used internally by send() to convert IDN domain parts before the pre-SMTPUTF8 ASCII regex check; surfaced publicly so operators wiring custom transports can apply the same normalization.
var b = require("@blamejs/core");
var ascii = b.mail.toAscii("münchen.de");
// → "xn--mnchen-3ya.de"
b.mail.toUnicode(domain) #
Decode an ASCII-Compatible-Encoding (Punycode xn--…) domain back to its Unicode form. Returns null when the input isn't a valid IDN domain. Operators rendering received-from / authentication- results trace lines use this to display the human-readable form alongside the on-the-wire ASCII representation.
var b = require("@blamejs/core");
var u = b.mail.toUnicode("xn--mnchen-3ya.de");
// → "münchen.de"
b.mail.reverseDns(ip) #
Forward-confirmed reverse DNS lookup (FCrDNS, RFC 8601 §3 lite) for an IPv4 or IPv6 address. Returns { ok, ptr, forward, fcrdns }:
ok— whether the PTR resolved at all.ptr— the first PTR record name (ornull).forward— array of A / AAAA addresses for that name (or[]).fcrdns—truewhen the originalipappears inforward.
Used as the building block for the iprev mail-authentication check (RFC 8601 §2.7.3): a sender's connect-IP must reverse-resolve to a PTR name whose forward A/AAAA includes that IP. Operators wiring inbound mail-receive paths call this on the connect address before accepting the SMTP transaction; bulk-sender reputation systems use the same check for outbound submission.
Errors thrown by the underlying DNS path (bad-IP shape / lookup timeout) are caught and surfaced as { ok: false, error: code } so the call doesn't reject the inbound path on a transient DNS blip; fcrdns remains false.
var b = require("@blamejs/core");
var r = await b.mail.reverseDns("8.8.8.8");
// → { ok: true, ptr: "dns.google", forward: ["8.8.8.8"], fcrdns: true }
b.mail.create(opts) #
{
transport: function (message) | { send(message), name? }, // default: console
defaults: { from, replyTo, headers, ... }, // merged into every message
audit: boolean, // default true
commercial: boolean, // CAN-SPAM §7704 enforcement
regulated: boolean, // alias for commercial:true
postalAddress: { street, city, region, postalCode, country } | string,
footerSeparator: string, // default "\n\n----\n" / "
"
footerHtml: string, // override for html-part footer
}
Build a mail instance bound to a transport + defaults. Returns { send, transport, defaults }: send(message) validates the merged message against the framework contract, applies CAN-SPAM footer + unsubscribe enforcement when commercial: true, runs RFC 8058 List-Unsubscribe header expansion when the message carries unsubscribe, then delegates to the transport. Audit rows record recipient counts only (addresses are PII).
var b = require("@blamejs/core");
var mail = b.mail.create({
transport: b.mail.transports.memory(),
defaults: { from: "Acme " },
});
// → { send, transport, defaults }
b.mail.feedbackId(opts) #
{
campaignId: string, // operator's campaign tag (e.g. "wk26-promo")
customerId: string, // operator's tenant or user-segment id
mailType: string, // operator-defined message type (e.g. "marketing")
senderId: string, // operator's app / IP-pool / domain reputation id
}
Build a Gmail Feedback Loop (FBL) Feedback-ID header value per Google's FBL convention: a colon-separated 4-tuple CampaignID:CustomerID:MailType:SenderID. Setting Feedback-ID on outbound mail lets Gmail surface per-campaign abuse-rate metrics back via the Postmaster Tools API so operators see spam complaints aggregated by their own campaign vocabulary instead of by SMTP envelope-sender alone.
Refuses missing / empty fields (mail/bad-feedback-id-field), fields containing : (would corrupt the 4-tuple separator), and fields longer than 64 bytes (Gmail truncates beyond ~64 chars per field). Operators set the result via mail.create({ headers: { "Feedback-ID": b.mail.feedbackId({...}) } }) or attach it to an individual send().
var feedbackId = b.mail.feedbackId({
campaignId: "wk26-promo",
customerId: "acme",
mailType: "marketing",
senderId: "mail-pool-1",
});
// → "wk26-promo:acme:marketing:mail-pool-1"
mail.send({
to: "...",
headers: { "Feedback-ID": feedbackId },
});
Last updated 2026-08-08T16:39:49.652Z by seeder.