Mail MX Server
Inbound SMTP / MX listener. Composes the framework's existing mail-gate substrates (b.mail.helo, b.mail.rbl, b.mail.greylist, b.guardEnvelope, b.mail.auth.dmarc, b.safeMime, b.guardEmail, b.guardSmtpCommand, b.mail.agent) into one operator-facing server that accepts inbound mail per RFC 5321 with PQC-shaped TLS posture, SMTP- smuggling defense baked into the wire-protocol layer, and the gate cascade running at the right phase of the state machine.
create({ ... }).listen() binds the TCP port; every incoming connection drives the CONNECT → EHLO → [STARTTLS → EHLO] → MAIL → RCPT (×N) → DATA → DATA-body → QUIT state machine. Each phase passes through the operator-supplied gates (defaulting to "no-op" when the operator hasn't wired a gate) and refuses with the appropriate 5xx (permanent) or 4xx (transient) SMTP reply code on gate fail.
## Defenses baked in
- **SMTP smuggling** (CVE-2023-51764 Postfix / CVE-2023-51765 Sendmail / CVE-2023-51766 Exim) — every wire line passes through b.guardSmtpCommand.validate which refuses bare LF, bare CR, NUL, C0 controls, DEL, and oversize. The DATA body's \r\n.\r\n terminator is matched on canonical CRLF only — bare-LF dot-terminators are refused. Together this defends the CVE-2023-51764 class where a hostile sender smuggles a second message past the framework's filter by terminating the first one with \n.\n instead of \r\n.\r\n.
- **Open-relay defense** — RCPT TO non-local refused with 550 5.7.1 Relaying denied unless the operator explicitly registered the destination via relayAllowedFor: [{ cidr, scope }]. The default posture is "MX-only, no relay" so a misconfigured boot can't accidentally become an open relay.
- **STARTTLS stripping (CVE-2021-38371 Exim, CVE-2021-33515 Dovecot)** — once STARTTLS is advertised + selected, subsequent commands MUST run over the negotiated TLS context. A pre-STARTTLS pipelining attempt (RFC 2920) to inject commands that take effect post-handshake is refused by clearing the command buffer at STARTTLS time and reading fresh from the TLS socket only — defends both the Exim and Dovecot variants of the STARTTLS-injection class.
- **Resource exhaustion** — per-command line cap (default 1 KiB), DATA body cap (default 50 MiB per RFC 5321 §4.5.3.1.7), per-recipient cap (default 100 per RFC 5321 §4.5.3.1.8), connection idle timeout (default 5 minutes per RFC 5321 §4.5.3.2.7). Operator opts up with explicit bounds.
- **TLS posture** — tlsContext MUST be supplied (no implicit plaintext-only mode). Operator passes a b.network.tls.context output which carries the framework's TLS 1.3 default + OCSP / CT-log posture. Pre-STARTTLS plain commands are limited to EHLO / HELO / STARTTLS / NOOP / QUIT / RSET; MAIL / RCPT / DATA all refused with 530 5.7.0 Must issue a STARTTLS command first.
## Audit lifecycle
mail.server.mx.connect— IP, TLS state, FCrDNS hostnamemail.server.mx.helo— HELO greeting, helo-gate verdictmail.server.mx.helo_gate_refused— HELO identity refused (gate action)mail.server.mx.mail_from— sender addressmail.server.mx.rcpt_to— recipient, rblListed flag, greylist actionmail.server.mx.rbl_refused— connecting IP on a DNS blocklist (zones)mail.server.mx.greylist_deferred— (ip, from, rcpt) first-seen 450 deferralmail.server.mx.data_refused— refusal reason + SMTP code (5xx vs 4xx)mail.server.mx.envelope_verdict— DATA-phase SPF/DKIM/DMARC results + action (accept / quarantine / reject / defer) + gate modemail.server.mx.envelope_error— DATA-phase authentication pipeline failure or timeout (disposition follows onTemperror)mail.server.mx.delivered— agent.handoff ackmail.server.mx.tls_handshake_failed— handshake errormail.server.mx.smtp_smuggling_detected— CRLF.CRLF injection classmail.server.mx.relay_refused— open-relay attempt
## What v1 does NOT ship
- **AUTH / submission auth** — MX listener is inbound from the internet, no authentication. Submission listener (port 587) is a separate slice with SCRAM-SHA-256 / XOAUTH2 / EXTERNAL. - **Sieve filtering** — composes via b.mail.agent at delivery time; the MX listener doesn't decide policy itself. - **Outbound DSN generation** — b.guardDsn parses inbound DSNs; outbound DSN emission deferred to the submission slice. - **8BITMIME** (RFC 6152, obsoletes RFC 1652) — advertised in the EHLO capabilities since the DATA body parser via b.safeMime is octet-clean; no transcoding needed. - **SMTPUTF8** (RFC 6531) + **IDN** (RFC 5891) — the wire-protocol layer here is encoding-agnostic; SMTPUTF8 capability advertisement is a follow-up slice once the operator's downstream (mail-store + delivery agent) accepts Unicode mailbox-local-part bytes. Today the listener does not advertise SMTPUTF8 and refuses non-ASCII in MAIL FROM / RCPT TO via b.guardSmtpCommand.
## Composition contract
Every gate is a primitive that already exists. The MX slice is a state-machine + wire-protocol coordinator — no new crypto, no new parsing, no new RFC-layer primitives. When the operator doesn't wire a gate (e.g. omits opts.greylist), the listener skips that phase rather than synthesizing a verdict.
Connection-level gates are wired into the live state machine: opts.helo (HELO identity) evaluates at HELO/EHLO; opts.rbl (connecting-IP DNS blocklist, evaluated once per connection) and opts.greylist ((ip, from, rcpt) first-seen deferral) evaluate at RCPT TO and surface their verdicts on the rcpt_to event. The message-authentication gate (opts.guardEnvelope) runs at DATA completion through b.mail.inbound.verify — SPF (RFC 7208) on the envelope identity, DKIM (RFC 6376) on the message bytes, DMARC (RFC 7489) policy + alignment on the From-header domain — and in enforce mode refuses before the agent handoff: 550 5.7.26 (RFC 7372) when the sender's published policy says reject, 550 5.7.1 on the RFC 7489 §6.6.1 multi-From spoofing shape, 451 4.7.0 on DNS temperror or pipeline timeout (operator-tunable via onTemperror / timeoutMs). Accepted messages carry the verdict to the agent handoff as auth and gain the receiver's RFC 8601 Authentication-Results header — any sender-attached header forging this receiver's authserv-id is stripped first (§5) — so downstream consumers act on authenticated results instead of re-verifying; monitor mode annotates without refusing.
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 });
Last updated 2026-08-08T16:39:49.652Z by seeder.