Mail IMAP Server
IMAP4rev2 mailbox-access listener (RFC 9051; obsoletes RFC 3501). Modern MUAs (Thunderbird, Apple Mail, mutt, K-9, FairEmail, etc.) connect here to read + manage messages without operators running dovecot / cyrus alongside. Composes the framework's existing substrates:
- b.guardImapCommand for wire-protocol shape + smuggling defense (literal-injection, bare-CR/LF refusal, per-verb shape, RFC 9051 §2.2.2 literal framing) - b.mail.server.rateLimit for per-IP DoS defense (concurrent + rate + AUTH-failure budget + slow-loris) - b.mailStore (operator-supplied backend) for the actual mail storage + UIDVALIDITY + modseq tracking - operator-supplied authenticator for SASL credential verify - b.mail.server.tls recommended for cert + key loading + rotation
## State machine (RFC 9051 §3)
NOT-AUTHENTICATED → [STARTTLS → NOT-AUTH-TLS] → AUTH/LOGIN →
AUTHENTICATED ↔ SELECTED → LOGOUT
↑ EXAMINE ↓ CLOSE / UNSELECT
Commands gated by state:
- NOT-AUTHENTICATED: STARTTLS / AUTHENTICATE / LOGIN / NOOP / CAPABILITY / LOGOUT / ID - AUTHENTICATED: SELECT / EXAMINE / CREATE / DELETE / RENAME / SUBSCRIBE / UNSUBSCRIBE / LIST / STATUS / APPEND / NAMESPACE / IDLE / ENABLE / NOOP / CAPABILITY / LOGOUT / ID - SELECTED: CHECK / CLOSE / UNSELECT / EXPUNGE / SEARCH / FETCH / STORE / COPY / MOVE / UID … / IDLE / NOOP / CAPABILITY / LOGOUT + every AUTHENTICATED command
Tagged response model: every client command carries a tag (A001 LOGIN …); server replies with one or more untagged responses (* …) then A001 OK … / A001 NO … / A001 BAD ….
## Wire-protocol defenses
- **STARTTLS stripping (CVE-2021-33515 Dovecot class)** — STARTTLS upgrade clears pre-handshake receive buffer; any pipelined command queued before TLS is refused with BAD Pipelined post-STARTTLS not permitted.
- **Literal-injection / command-continuation smuggling** — {n} literal continuation MUST come on a line of its own (per b.guardImapCommand.detectLiteralSmuggling); oversize literals refused (default 64 MiB); LITERAL+ (RFC 7888) non- synchronizing literals only honored post-AUTH.
- **Mailbox-name traversal** — mailbox path components validated through _validateMailboxName: refuses .., NUL, control chars, oversize. UTF-8 mailbox names (RFC 9051 §5.1) accepted; modified-UTF7 (RFC 3501 §5.1.3 legacy) refused unless allowLegacyMUtf7: true.
- **APPEND-flood** — per-tenant byte/sec cap surfaces via the b.mail.server.rateLimit's minBytesPerSecond floor on the APPEND-literal-body phase (same shape the MX listener uses for DATA-body).
- **Resource exhaustion** — per-line cap (default 8 KiB sans literal payload), per-literal cap (64 MiB), per-connection idle cap (default 30 min when not in IDLE; IDLE itself capped at 29 min per RFC 2177 §3 to force re-issue).
- **Connection-rate + AUTH-failure budget** — composes b.mail.server.rateLimit. Each AUTH failure increments the budget; trip the cap and new AUTH attempts get * BAD Too many AUTH failures + connection close.
## Audit lifecycle
mail.server.imap.connect— IP, TLS statemail.server.imap.auth_attempt— mechanism, actor-hashmail.server.imap.auth_success— mechanism, tenantId, scopesmail.server.imap.auth_failed— mechanism, reasonmail.server.imap.select— mailbox, modseq, exists countmail.server.imap.append— mailbox, size, flagsmail.server.imap.fetch_bulk— sequence-set size, BODY partsmail.server.imap.expunge— count, modseqmail.server.imap.literal_overflow_refused— attempt size, capmail.server.imap.rate_limit_refused— IP, reasonmail.server.imap.smtp_smuggling_detected— literal-injection
## What v1 does NOT ship
- **SEARCH** — operator wires opts.search(actor, mailbox, query) when ready; the listener emits BAD search-not-configured until then. SEARCH expressions are operator-domain logic against the mailStore index. - **NOTIFY (RFC 5465)**, **METADATA (RFC 5464)**, **CATENATE (RFC 4469)**, **URLAUTH (RFC 4467)**, **IMAPSIEVE (RFC 6785)**, **COMPRESS=DEFLATE (RFC 4978)** — opt-in / refused. - **CONDSTORE / QRESYNC (RFC 7162)** — modseq is exposed via STATUS but per-FETCH CHANGEDSINCE delta is operator-side follow-up.
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 });
Last updated 2026-08-08T16:39:49.652Z by seeder.