Mail POP3 Server
POP3 mailbox-access listener (RFC 1939 + RFC 2449 capabilities + RFC 2595 STLS + RFC 5034 SASL AUTH). Opt-in legacy fallback for MUAs that don't speak IMAP — the framework's blamepost roadmap makes JMAP primary and IMAP/POP3 opt-ins; this listener exists so operators with last-decade MUAs (older Outlook profiles, legacy mobile clients, simple device firmware) can still authenticate + pull messages.
## State machine (RFC 1939 §3)
AUTHORIZATION → TRANSACTION → UPDATE → (close)
- **AUTHORIZATION**: STLS / CAPA / USER / PASS / APOP / AUTH / QUIT. After successful USER+PASS / APOP / AUTH the connection enters TRANSACTION. - **TRANSACTION**: STAT / LIST / RETR / DELE / NOOP / RSET / TOP / UIDL / QUIT. DELE marks messages for deletion; actual deletion happens in UPDATE state on QUIT. - **UPDATE**: triggered by QUIT from TRANSACTION; the listener calls mailStore.commitPop3Drop(actor, dropId) to apply the pending deletes atomically, then closes.
## Wire-protocol defenses
- **Cleartext-auth refusal under strict** — RFC 1939 USER/PASS sends the password in plaintext. Strict + balanced profiles refuse USER/PASS pre-TLS; operators with legacy clients pass profile: "permissive".
- **STLS injection (CVE-2021-33515 class)** — STLS upgrade clears pre-handshake receive buffer; any pipelined command queued before TLS is dropped.
- **APOP refusal under strict** — RFC 1939 §7 APOP uses MD5 challenge-response. M³AAWG / NIST SP 800-131A r2 phase out MD5; the strict profile refuses APOP.
- **Per-IP rate limit + AUTH-failure budget** — composes b.mail.server.rateLimit (default-on). The submission listener's authFailuresPerIpPer15Min cap applies to USER+PASS / APOP / AUTH refusals.
- **Slow-loris on RETR / TOP** — per-connection idleTimeoutMs bounds dead connections; b.mail.server.rateLimit.minBytesPerSecond bounds trickle-receive class.
## Audit lifecycle
mail.server.pop3.connect— IP, TLS statemail.server.pop3.auth_attempt— verb, actor-hashmail.server.pop3.auth_success— verb, tenantIdmail.server.pop3.auth_failed— verb, reasonmail.server.pop3.auth_rate_limit_refusedmail.server.pop3.transaction_start— drop count, total sizemail.server.pop3.retr— msg-nummail.server.pop3.dele— msg-num (marked-for-delete)mail.server.pop3.update_commit— final-deleted countmail.server.pop3.rate_limit_refused— IP, reason
## What v1 does NOT ship
- **APOP** — refused under strict + balanced; permissive opts in. APOP uses MD5; modern deployments use TLS + USER/PASS or SASL instead. - **SASL mechanisms beyond PLAIN** — CRAM-MD5 / SCRAM-SHA-256 / OAUTHBEARER all wire through operator's auth.verify. v1 advertises PLAIN only; operators add via auth.mechanisms. - **Multi-step SASL exchange** — single-step PLAIN is sufficient for the v1 surface; SCRAM round-trip ships when an operator surfaces demand. - **Per-message lock** — POP3 has no native message-id beyond UIDL; concurrent connections from the same actor compete via mailStore.openPop3Drop({ exclusive: true }).
## Composition contract
- b.guardPop3Command — wire-protocol gate - b.mail.server.rateLimit — DoS defense - b.mailStore — operator-supplied backend (must expose openPop3Drop(actor, opts) / commitPop3Drop(actor, dropId) / getMessage(actor, dropId, msgNum, { headersOnly?, headerLines? }) / listMessages(actor, dropId) / markDelete(actor, dropId, msgNum)) - operator's auth.verify(mechanism, credentials) async predicate - b.network.tls.context — TLS posture
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 });
Last updated 2026-08-08T16:39:49.652Z by seeder.