Mail Submission Server

Outbound SMTP submission listener per RFC 6409 (port 587) and RFC 8314 implicit-TLS submissions (port 465). Where the MX listener (b.mail.server.mx) accepts inbound mail from the internet to local mailboxes, the submission listener accepts outbound mail from authenticated MUAs / app-side mail-senders and routes it to upstream MXs via b.mail.send.

Differences from the MX listener:

- **AUTH required** — operator-supplied authenticator validates SASL credentials (PLAIN / LOGIN / SCRAM-SHA-256 / EXTERNAL / XOAUTH2). MAIL FROM is refused until AUTH succeeds.

- **Identity binding** — under strict profile, MAIL FROM: MUST match the authenticated actor's mailbox set; refused with 553 5.7.1 Sender address rejected. Permissive logs the mismatch but allows.

- **TLS required for AUTH** (RFC 4954 §4) — pre-STARTTLS AUTH refused with 538 5.7.11 Encryption required for AUTH mechanism. Permissive profile allows plaintext AUTH for legacy operator-acknowledged downgrade.

- **Implicit-TLS mode** — implicitTls: true wraps every connection in TLS from the SYN (port 465 per RFC 8314); no STARTTLS advertised because the connection is already secure.

- **Outbound routing** — successful DATA hands off to the operator-supplied agent.handoff({ ... }) for relay through b.mail.send to upstream MXs. The listener doesn't perform MX lookup or outbound delivery itself.

## Wire-protocol defenses (inherited from MX listener pattern)

- SMTP smuggling (CVE-2023-51764 / -51765 / -51766 / RFC 5321 §2.3.8): every wire line through b.guardSmtpCommand.validate; DATA-body terminator scan through b.safeSmtp.findDotTerminator (strict-CRLF); smuggling shape detected via b.guardSmtpCommand.detectBodySmuggling.

- STARTTLS-injection (CVE-2021-38371 Exim, CVE-2021-33515 Dovecot): command buffer cleared at upgrade time.

- Resource exhaustion: per-command line cap (1 KiB), DATA body cap (50 MiB per RFC 5321 §4.5.3.1.7), per-message recipient cap (100 per RFC 5321 §4.5.3.1.8), idle timeout (5 minutes per RFC 5321 §4.5.3.2.7).

## SMTP AUTH (RFC 4954)

- Mechanisms negotiated per RFC 4422 (SASL) — the operator opts the list auth.mechanisms into the EHLO advertisement. - Initial-response variant AUTH MECH (RFC 4954 §4) supported. - Failed AUTH emits mail.server.submission.auth_failed with mechanism + reason; operator's rate-limit wired via auth.rateLimit (composes b.middleware.rateLimit) trips 421 4.7.0 Too many failed AUTH after the operator-configured budget.

## Audit lifecycle (in addition to the MX listener's)

## What v1 does NOT ship

- **DKIM signing pre-relay** — operator wires b.mail.dkim.sign in their outbound agent. - **Per-actor outbound quota** — operator implements via b.dailyByteQuota against the authenticated actor.

(CHUNKING / BDAT, RFC 3030, IS supported — advertised in EHLO and handled alongside DATA.)

## Composition contract

Every gate is a primitive that already exists. Submission listener composes b.guardSmtpCommand (wire-protocol gate + smuggling defense), b.safeSmtp (wire-protocol parser), the operator's authenticator (SASL verify), b.mail.send (outbound MX routing), and the framework's TLS posture via b.network.tls.context.

b.mail.server.submission.create(opts) #

stable0.9.47
{
  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 });

Last updated 2026-08-08T16:39:49.652Z by seeder.