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)
mail.server.submission.auth_attempt— mechanism, actor-hash, remotemail.server.submission.auth_success— mechanism, tenantId, scopesmail.server.submission.auth_failed— mechanism, reasonmail.server.submission.identity_mismatch— auth identity vs MAIL FROMmail.server.submission.outbound_routed— delivery agent ack
## 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) #
{
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.