@blamejs/core 0.9.49 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +952 -908
  2. package/index.js +25 -0
  3. package/lib/_test/crypto-fixtures.js +67 -0
  4. package/lib/agent-event-bus.js +52 -6
  5. package/lib/agent-idempotency.js +169 -16
  6. package/lib/agent-orchestrator.js +263 -9
  7. package/lib/agent-posture-chain.js +163 -5
  8. package/lib/agent-saga.js +146 -16
  9. package/lib/agent-snapshot.js +349 -19
  10. package/lib/agent-stream.js +34 -2
  11. package/lib/agent-tenant.js +179 -23
  12. package/lib/agent-trace.js +84 -21
  13. package/lib/auth/aal.js +8 -1
  14. package/lib/auth/ciba.js +6 -1
  15. package/lib/auth/dpop.js +7 -2
  16. package/lib/auth/fal.js +17 -8
  17. package/lib/auth/jwt-external.js +128 -4
  18. package/lib/auth/oauth.js +232 -10
  19. package/lib/auth/oid4vci.js +67 -7
  20. package/lib/auth/openid-federation.js +71 -25
  21. package/lib/auth/passkey.js +140 -6
  22. package/lib/auth/sd-jwt-vc.js +78 -5
  23. package/lib/circuit-breaker.js +10 -2
  24. package/lib/cli.js +13 -0
  25. package/lib/compliance.js +176 -8
  26. package/lib/crypto-field.js +114 -14
  27. package/lib/crypto.js +216 -20
  28. package/lib/db.js +1 -0
  29. package/lib/guard-graphql.js +37 -0
  30. package/lib/guard-jmap.js +321 -0
  31. package/lib/guard-managesieve-command.js +566 -0
  32. package/lib/guard-pop3-command.js +317 -0
  33. package/lib/guard-regex.js +138 -1
  34. package/lib/guard-smtp-command.js +58 -3
  35. package/lib/guard-xml.js +39 -1
  36. package/lib/mail-agent.js +20 -7
  37. package/lib/mail-arc-sign.js +12 -8
  38. package/lib/mail-auth.js +323 -34
  39. package/lib/mail-crypto-pgp.js +934 -0
  40. package/lib/mail-crypto-smime.js +340 -0
  41. package/lib/mail-crypto.js +108 -0
  42. package/lib/mail-dav.js +1224 -0
  43. package/lib/mail-deploy.js +492 -0
  44. package/lib/mail-dkim.js +431 -26
  45. package/lib/mail-journal.js +435 -0
  46. package/lib/mail-scan.js +502 -0
  47. package/lib/mail-server-imap.js +64 -26
  48. package/lib/mail-server-jmap.js +488 -0
  49. package/lib/mail-server-managesieve.js +853 -0
  50. package/lib/mail-server-mx.js +40 -30
  51. package/lib/mail-server-pop3.js +836 -0
  52. package/lib/mail-server-rate-limit.js +13 -0
  53. package/lib/mail-server-submission.js +70 -24
  54. package/lib/mail-server-tls.js +445 -0
  55. package/lib/mail-sieve.js +557 -0
  56. package/lib/mail-spam-score.js +284 -0
  57. package/lib/mail.js +99 -0
  58. package/lib/metrics.js +80 -3
  59. package/lib/middleware/dpop.js +58 -3
  60. package/lib/middleware/idempotency-key.js +255 -42
  61. package/lib/middleware/protected-resource-metadata.js +114 -2
  62. package/lib/network-dns-resolver.js +33 -0
  63. package/lib/network-tls.js +46 -0
  64. package/lib/otel-export.js +13 -4
  65. package/lib/outbox.js +62 -12
  66. package/lib/pqc-agent.js +13 -5
  67. package/lib/retry.js +23 -9
  68. package/lib/router.js +23 -1
  69. package/lib/safe-ical.js +634 -0
  70. package/lib/safe-icap.js +502 -0
  71. package/lib/safe-mime.js +15 -0
  72. package/lib/safe-sieve.js +684 -0
  73. package/lib/safe-smtp.js +57 -0
  74. package/lib/safe-url.js +37 -0
  75. package/lib/safe-vcard.js +473 -0
  76. package/lib/self-update-standalone-verifier.js +32 -3
  77. package/lib/self-update.js +153 -33
  78. package/lib/vendor/MANIFEST.json +161 -156
  79. package/lib/vendor-data.js +127 -9
  80. package/lib/vex.js +324 -59
  81. package/package.json +1 -1
  82. package/sbom.cdx.json +6 -6
@@ -0,0 +1,836 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.mail.server.pop3
4
+ * @nav Mail
5
+ * @title Mail POP3 Server
6
+ * @order 550
7
+ *
8
+ * @intro
9
+ * POP3 mailbox-access listener (RFC 1939 + RFC 2449 capabilities +
10
+ * RFC 2595 STLS + RFC 5034 SASL AUTH). Opt-in legacy fallback for
11
+ * MUAs that don't speak IMAP — the framework's blamepost roadmap
12
+ * makes JMAP primary and IMAP/POP3 opt-ins; this listener exists
13
+ * so operators with last-decade MUAs (older Outlook profiles,
14
+ * legacy mobile clients, simple device firmware) can still
15
+ * authenticate + pull messages.
16
+ *
17
+ * ## State machine (RFC 1939 §3)
18
+ *
19
+ * ```
20
+ * AUTHORIZATION → TRANSACTION → UPDATE → (close)
21
+ * ```
22
+ *
23
+ * - **AUTHORIZATION**: STLS / CAPA / USER / PASS / APOP / AUTH /
24
+ * QUIT. After successful USER+PASS / APOP / AUTH the connection
25
+ * enters TRANSACTION.
26
+ * - **TRANSACTION**: STAT / LIST / RETR / DELE / NOOP / RSET / TOP /
27
+ * UIDL / QUIT. DELE marks messages for deletion; actual deletion
28
+ * happens in UPDATE state on QUIT.
29
+ * - **UPDATE**: triggered by QUIT from TRANSACTION; the listener
30
+ * calls `mailStore.commitPop3Drop(actor, dropId)` to apply the
31
+ * pending deletes atomically, then closes.
32
+ *
33
+ * ## Wire-protocol defenses
34
+ *
35
+ * - **Cleartext-auth refusal under strict** — RFC 1939 USER/PASS
36
+ * sends the password in plaintext. Strict + balanced profiles
37
+ * refuse USER/PASS pre-TLS; operators with legacy clients pass
38
+ * `profile: "permissive"`.
39
+ *
40
+ * - **STLS injection (CVE-2021-33515 class)** — STLS upgrade clears
41
+ * pre-handshake receive buffer; any pipelined command queued
42
+ * before TLS is dropped.
43
+ *
44
+ * - **APOP refusal under strict** — RFC 1939 §7 APOP uses MD5
45
+ * challenge-response. M³AAWG / NIST SP 800-131A r2 phase out
46
+ * MD5; the strict profile refuses APOP.
47
+ *
48
+ * - **Per-IP rate limit + AUTH-failure budget** — composes
49
+ * `b.mail.server.rateLimit` (default-on). The submission listener's
50
+ * `authFailuresPerIpPer15Min` cap applies to USER+PASS / APOP /
51
+ * AUTH refusals.
52
+ *
53
+ * - **Slow-loris on RETR / TOP** — per-connection `idleTimeoutMs`
54
+ * bounds dead connections; `b.mail.server.rateLimit.minBytesPerSecond`
55
+ * bounds trickle-receive class.
56
+ *
57
+ * ## Audit lifecycle
58
+ *
59
+ * - `mail.server.pop3.connect` — IP, TLS state
60
+ * - `mail.server.pop3.auth_attempt` — verb, actor-hash
61
+ * - `mail.server.pop3.auth_success` — verb, tenantId
62
+ * - `mail.server.pop3.auth_failed` — verb, reason
63
+ * - `mail.server.pop3.auth_rate_limit_refused`
64
+ * - `mail.server.pop3.transaction_start` — drop count, total size
65
+ * - `mail.server.pop3.retr` — msg-num
66
+ * - `mail.server.pop3.dele` — msg-num (marked-for-delete)
67
+ * - `mail.server.pop3.update_commit` — final-deleted count
68
+ * - `mail.server.pop3.rate_limit_refused` — IP, reason
69
+ *
70
+ * ## What v1 does NOT ship
71
+ *
72
+ * - **APOP** — refused under strict + balanced; permissive opts in.
73
+ * APOP uses MD5; modern deployments use TLS + USER/PASS or SASL
74
+ * instead.
75
+ * - **SASL mechanisms beyond PLAIN** — CRAM-MD5 / SCRAM-SHA-256 /
76
+ * OAUTHBEARER all wire through operator's `auth.verify`. v1
77
+ * advertises PLAIN only; operators add via `auth.mechanisms`.
78
+ * - **Multi-step SASL exchange** — single-step PLAIN is sufficient
79
+ * for the v1 surface; SCRAM round-trip ships when an operator
80
+ * surfaces demand.
81
+ * - **Per-message lock** — POP3 has no native message-id beyond
82
+ * UIDL; concurrent connections from the same actor compete via
83
+ * `mailStore.openPop3Drop({ exclusive: true })`.
84
+ *
85
+ * ## Composition contract
86
+ *
87
+ * - `b.guardPop3Command` — wire-protocol gate
88
+ * - `b.mail.server.rateLimit` — DoS defense
89
+ * - `b.mailStore` — operator-supplied backend (must expose
90
+ * `openPop3Drop(actor, opts)` / `commitPop3Drop(actor, dropId)` /
91
+ * `getMessage(actor, dropId, msgNum, { headersOnly?, headerLines? })` /
92
+ * `listMessages(actor, dropId)` / `markDelete(actor, dropId, msgNum)`)
93
+ * - operator's `auth.verify(mechanism, credentials)` async predicate
94
+ * - `b.network.tls.context` — TLS posture
95
+ *
96
+ * @card
97
+ * POP3 mailbox-access listener (RFC 1939 + RFC 2449 + RFC 2595 +
98
+ * RFC 5034). Opt-in legacy fallback; state machine AUTH → TRANS →
99
+ * UPDATE. Composes b.guardPop3Command + b.mail.server.rateLimit +
100
+ * operator-supplied mailStore + SASL authenticator. STLS-injection
101
+ * defense + AUTH-failure budget + cleartext-auth refusal under
102
+ * strict.
103
+ */
104
+
105
+ var net = require("node:net");
106
+ var lazyRequire = require("./lazy-require");
107
+ var C = require("./constants");
108
+ var bCrypto = require("./crypto");
109
+ var numericBounds = require("./numeric-bounds");
110
+ var validateOpts = require("./validate-opts");
111
+ var guardPop3Command = require("./guard-pop3-command");
112
+ var mailServerRateLimit = require("./mail-server-rate-limit");
113
+ var mailServerTls = require("./mail-server-tls");
114
+ var safeSmtp = require("./safe-smtp");
115
+ var { defineClass } = require("./framework-error");
116
+
117
+ var audit = lazyRequire(function () { return require("./audit"); });
118
+
119
+ var MailServerPop3Error = defineClass("MailServerPop3Error", { alwaysPermanent: true });
120
+
121
+ var DEFAULT_MAX_LINE_BYTES = 1024; // allow:raw-byte-literal — RFC 2449 §4 line cap (permissive)
122
+ var DEFAULT_IDLE_TIMEOUT_MS = C.TIME.minutes(10);
123
+ var DEFAULT_GREETING_VENDOR = "blamejs POP3";
124
+
125
+ var ERR_CLAMP = 200; // allow:raw-byte-literal — protocol-reply error-message clamp
126
+
127
+ /**
128
+ * @primitive b.mail.server.pop3.create
129
+ * @signature b.mail.server.pop3.create(opts)
130
+ * @since 0.9.52
131
+ * @status stable
132
+ * @related b.mail.server.imap.create, b.mail.server.submission.create, b.mailStore.create
133
+ *
134
+ * Build a POP3 listener (RFC 1939). Returns a handle exposing
135
+ * `listen({ port, address })` and `close()`. POP3 is opt-in legacy —
136
+ * deployments should prefer `b.mail.server.imap` + `b.mail.server.jmap`
137
+ * for new MUAs.
138
+ *
139
+ * @opts
140
+ * tlsContext: SecureContext, // required (no plaintext)
141
+ * greeting: string, // default "blamejs POP3"
142
+ * maxLineBytes: number, // default 1024
143
+ * idleTimeoutMs: number, // default 10 min
144
+ * profile: "strict" | "balanced" | "permissive",
145
+ * auth: {
146
+ * mechanisms: ["PLAIN"], // SASL mechs to advertise
147
+ * verify: async function (mech, credentials) → { ok, actor },
148
+ * },
149
+ * mailStore: b.mailStore handle,
150
+ * rateLimit: b.mail.server.rateLimit handle | opts | false,
151
+ * audit: b.audit
152
+ *
153
+ * @example
154
+ * var pop3 = b.mail.server.pop3.create({
155
+ * tlsContext: b.mail.server.tls.context({ certFile, keyFile }).secureContext,
156
+ * auth: {
157
+ * mechanisms: ["PLAIN"],
158
+ * verify: async function (mech, creds) {
159
+ * return { ok: true, actor: { username: creds.authzid, tenantId: "t1" } };
160
+ * },
161
+ * },
162
+ * mailStore: b.mailStore.create({ backend: b.db.handle() }),
163
+ * });
164
+ * await pop3.listen({ port: 110 });
165
+ */
166
+ function create(opts) {
167
+ validateOpts.requireObject(opts, "mail.server.pop3.create",
168
+ MailServerPop3Error, "mail-server-pop3/bad-opts");
169
+ if (!opts.tlsContext) {
170
+ throw new MailServerPop3Error("mail-server-pop3/no-tls-context",
171
+ "mail.server.pop3.create: tlsContext is required (no implicit plaintext mode). " +
172
+ "Use b.mail.server.tls.context({ certFile, keyFile, watch: true }).");
173
+ }
174
+ if (!opts.mailStore || typeof opts.mailStore.openPop3Drop !== "function") {
175
+ throw new MailServerPop3Error("mail-server-pop3/no-mail-store",
176
+ "mail.server.pop3.create: mailStore is required (must expose openPop3Drop/commitPop3Drop/" +
177
+ "getMessage/listMessages/markDelete; compose b.mailStore.create or operator-supplied backend)");
178
+ }
179
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
180
+ ["maxLineBytes", "idleTimeoutMs"],
181
+ "mail.server.pop3.", MailServerPop3Error, "mail-server-pop3/bad-bound");
182
+
183
+ var greeting = opts.greeting || DEFAULT_GREETING_VENDOR;
184
+ var maxLineBytes = opts.maxLineBytes || DEFAULT_MAX_LINE_BYTES;
185
+ var idleTimeoutMs = opts.idleTimeoutMs || DEFAULT_IDLE_TIMEOUT_MS;
186
+ var profile = opts.profile || "strict";
187
+ var authConfig = opts.auth || null;
188
+ var mailStore = opts.mailStore;
189
+
190
+ var rateLimit;
191
+ if (opts.rateLimit === false) {
192
+ rateLimit = mailServerRateLimit.create({ disabled: true });
193
+ } else if (opts.rateLimit && typeof opts.rateLimit.admitConnection === "function") {
194
+ rateLimit = opts.rateLimit;
195
+ } else {
196
+ rateLimit = mailServerRateLimit.create(opts.rateLimit || {});
197
+ }
198
+
199
+ var tcpServer = null;
200
+ var listening = false;
201
+ var connections = new Set();
202
+
203
+ function _emit(action, metadata, outcome) {
204
+ try {
205
+ audit().safeEmit({
206
+ action: action,
207
+ outcome: outcome || "success",
208
+ metadata: metadata || {},
209
+ });
210
+ } catch (_e) { /* drop-silent */ }
211
+ }
212
+
213
+ function _handleConnection(rawSocket) {
214
+ var remoteAddress = rawSocket.remoteAddress || "0.0.0.0";
215
+ var admit = rateLimit.admitConnection(remoteAddress);
216
+ if (!admit.ok) {
217
+ _emit("mail.server.pop3.rate_limit_refused",
218
+ { remoteAddress: remoteAddress, reason: admit.reason }, "denied");
219
+ try { rawSocket.write("-ERR Too many connections from your IP\r\n"); }
220
+ catch (_e) { /* socket may be down */ }
221
+ try { rawSocket.destroy(); } catch (_e2) { /* idempotent */ }
222
+ return;
223
+ }
224
+ var connectionId = "pop3conn-" + bCrypto.generateToken(8); // allow:raw-byte-literal — connection-id length
225
+ var socket = rawSocket;
226
+ connections.add(socket);
227
+ // Single close handler covers BOTH operator-driven `_close(socket)`
228
+ // and client-initiated disconnects (TCP FIN / RST without a
229
+ // server-side close call) — releases the rate-limit slot AND
230
+ // removes the socket from the tracking set so it can't accumulate
231
+ // stale entries across long-lived deployments.
232
+ rawSocket.once("close", function () {
233
+ rateLimit.releaseConnection(remoteAddress);
234
+ connections.delete(socket);
235
+ });
236
+
237
+ var state = {
238
+ id: connectionId,
239
+ remoteAddress: remoteAddress,
240
+ tls: false,
241
+ stage: "authorization",
242
+ actor: null,
243
+ tentativeUser: null, // USER name pending PASS
244
+ dropId: null, // mailStore-issued drop handle on TRANSACTION entry
245
+ lineBuffer: Buffer.alloc(0),
246
+ };
247
+
248
+ _emit("mail.server.pop3.connect",
249
+ { connectionId: connectionId, remoteAddress: remoteAddress });
250
+
251
+ socket.setTimeout(idleTimeoutMs);
252
+ socket.on("timeout", function () {
253
+ _writeErr(socket, "Idle timeout");
254
+ _close(socket);
255
+ });
256
+ socket.on("error", function (err) {
257
+ _emit("mail.server.pop3.socket_error",
258
+ { connectionId: connectionId, error: (err && err.message) || String(err) }, "failure");
259
+ });
260
+
261
+ _writeOk(socket, greeting + " ready");
262
+
263
+ socket.on("data", function (chunk) {
264
+ state.lineBuffer = Buffer.concat([state.lineBuffer, chunk]);
265
+ _drainBuffer(state, socket);
266
+ });
267
+ }
268
+
269
+ function _drainBuffer(state, socket) {
270
+ while (true) {
271
+ var crlf = state.lineBuffer.indexOf("\r\n");
272
+ if (crlf === -1) {
273
+ if (state.lineBuffer.length > maxLineBytes) {
274
+ _writeErr(socket, "Line too long (cap " + maxLineBytes + ")");
275
+ _close(socket);
276
+ }
277
+ return;
278
+ }
279
+ var rawLine = state.lineBuffer.subarray(0, crlf).toString("utf8");
280
+ state.lineBuffer = state.lineBuffer.subarray(crlf + 2);
281
+ _handleLine(state, socket, rawLine);
282
+ if (state.stage === "closed") return;
283
+ }
284
+ }
285
+
286
+ function _handleLine(state, socket, line) {
287
+ var parsed;
288
+ try {
289
+ parsed = guardPop3Command.validate(line, {
290
+ profile: profile,
291
+ tls: state.tls,
292
+ });
293
+ } catch (e) {
294
+ _writeErr(socket, (e && e.message ? e.message.slice(0, ERR_CLAMP) : "syntax"));
295
+ return;
296
+ }
297
+ _dispatch(state, socket, parsed);
298
+ }
299
+
300
+ function _dispatch(state, socket, parsed) {
301
+ var verb = parsed.verb;
302
+ var args = parsed.args;
303
+ switch (verb) {
304
+ case "CAPA": return _handleCapa(state, socket);
305
+ case "STLS": return _handleStls(state, socket);
306
+ case "USER": return _handleUser(state, socket, args);
307
+ case "PASS": return _handlePass(state, socket, args);
308
+ case "APOP": return _handleApop(state, socket, args);
309
+ case "AUTH": return _handleAuth(state, socket, args);
310
+ case "QUIT": return _handleQuit(state, socket);
311
+ case "STAT": return _handleStat(state, socket);
312
+ case "LIST": return _handleList(state, socket, args);
313
+ case "RETR": return _handleRetr(state, socket, args);
314
+ case "DELE": return _handleDele(state, socket, args);
315
+ case "NOOP": return _writeOk(socket, "noop");
316
+ case "RSET": return _handleRset(state, socket);
317
+ case "TOP": return _handleTop(state, socket, args);
318
+ case "UIDL": return _handleUidl(state, socket, args);
319
+ default: return _writeErr(socket, "Verb '" + verb + "' not implemented");
320
+ }
321
+ }
322
+
323
+ function _handleCapa(state, socket) {
324
+ _writeOk(socket, "Capability list follows");
325
+ socket.write("TOP\r\n");
326
+ socket.write("UIDL\r\n");
327
+ socket.write("RESP-CODES\r\n");
328
+ if (!state.tls) socket.write("STLS\r\n");
329
+ // Advertise AUTH mechanisms ONLY when wired (Codex P2 IMAP lesson:
330
+ // don't hardcode SASL mechs in caps).
331
+ if (authConfig && Array.isArray(authConfig.mechanisms) && authConfig.mechanisms.length > 0) {
332
+ var mechs = authConfig.mechanisms.map(function (m) {
333
+ return String(m).toUpperCase();
334
+ }).join(" ");
335
+ socket.write("SASL " + mechs + "\r\n");
336
+ }
337
+ socket.write("IMPLEMENTATION blamejs\r\n");
338
+ socket.write(".\r\n");
339
+ }
340
+
341
+ function _handleStls(state, socket) {
342
+ if (state.tls) {
343
+ _writeErr(socket, "STLS already negotiated");
344
+ return;
345
+ }
346
+ // RFC 2595 §4 — STLS is only valid in AUTHORIZATION state. Once
347
+ // a session has reached TRANSACTION (authenticated, with a drop
348
+ // lock against the mailbox), a TLS upgrade mid-session would
349
+ // re-key without re-authenticating and produce undefined
350
+ // behaviour against open mailbox state.
351
+ if (state.stage !== "authorization") {
352
+ _writeErr(socket, "STLS only valid in AUTHORIZATION (RFC 2595 §4)");
353
+ return;
354
+ }
355
+ _writeOk(socket, "Begin TLS negotiation");
356
+ // Drain pre-handshake buffer (RFC 2595 §4 + CVE-2021-33515 class
357
+ // STLS-injection defense — any pipelined commands the client
358
+ // queued before the upgrade are discarded; post-TLS reads fresh).
359
+ // Listener-removal + idle-timeout re-arm live in the shared
360
+ // upgradeSocket helper (b.mail.server.tls.upgradeSocket).
361
+ state.lineBuffer = Buffer.alloc(0);
362
+ // POP3 doesn't have an authPending shape (the SASL state is local
363
+ // to _handleAuth), but reset tentativeUser so a USER pipelined
364
+ // pre-handshake cannot bind a post-handshake PASS.
365
+ state.tentativeUser = null;
366
+ mailServerTls.upgradeSocket({
367
+ plainSocket: socket,
368
+ secureContext: opts.tlsContext,
369
+ idleTimeoutMs: idleTimeoutMs,
370
+ onSecure: function (_tlsSocket) { state.tls = true; },
371
+ onData: function (tlsSocket, chunk) {
372
+ state.lineBuffer = Buffer.concat([state.lineBuffer, chunk]);
373
+ _drainBuffer(state, tlsSocket);
374
+ },
375
+ onError: function (err) {
376
+ _emit("mail.server.pop3.tls_handshake_failed",
377
+ { connectionId: state.id, error: (err && err.message) || String(err) }, "failure");
378
+ _close(socket);
379
+ },
380
+ onTimeout: function (tlsSocket) {
381
+ _writeErr(tlsSocket, "Idle timeout");
382
+ _close(tlsSocket);
383
+ },
384
+ });
385
+ }
386
+
387
+ function _handleUser(state, socket, args) {
388
+ if (state.stage !== "authorization") {
389
+ _writeErr(socket, "USER only valid in AUTHORIZATION");
390
+ return;
391
+ }
392
+ if (state.actor) {
393
+ _writeErr(socket, "Already authenticated");
394
+ return;
395
+ }
396
+ // RFC 2595 §2.1 defense-in-depth — the guardPop3Command validator
397
+ // refuses USER over cleartext under strict at the wire boundary,
398
+ // but balanced/permissive operators previously reached this path
399
+ // and accepted a plaintext password. Refuse here too so a guard
400
+ // relax doesn't open BUG-11/MAIL-37 (cleartext credentials in
401
+ // POP3 USER/PASS) by composition. Permissive operators opt out
402
+ // by explicitly setting profile: "permissive".
403
+ if (!state.tls && profile !== "permissive") {
404
+ _emit("mail.server.pop3.auth_refused_cleartext",
405
+ { connectionId: state.id, verb: "USER", remoteAddress: state.remoteAddress },
406
+ "denied");
407
+ rateLimit.noteAuthFailure(state.remoteAddress);
408
+ _writeErr(socket, "USER refused over cleartext (use STLS first; RFC 2595 §2.1)");
409
+ return;
410
+ }
411
+ state.tentativeUser = args[0];
412
+ _writeOk(socket, "Send password");
413
+ }
414
+
415
+ function _handlePass(state, socket, args) {
416
+ if (state.stage !== "authorization" || !state.tentativeUser) {
417
+ _writeErr(socket, "PASS only valid after USER");
418
+ return;
419
+ }
420
+ if (!authConfig || typeof authConfig.verify !== "function") {
421
+ _writeErr(socket, "AUTH not configured on this listener");
422
+ return;
423
+ }
424
+ // BUG-11 / MAIL-37 — refuse PASS over cleartext when not permissive.
425
+ // USER already gated above, but this is defense-in-depth in case the
426
+ // USER guard was bypassed by a future codepath.
427
+ if (!state.tls && profile !== "permissive") {
428
+ _emit("mail.server.pop3.auth_refused_cleartext",
429
+ { connectionId: state.id, verb: "PASS", remoteAddress: state.remoteAddress },
430
+ "denied");
431
+ rateLimit.noteAuthFailure(state.remoteAddress);
432
+ _writeErr(socket, "PASS refused over cleartext (use STLS first; RFC 2595 §2.1)");
433
+ return;
434
+ }
435
+ var authAdmit = rateLimit.checkAuthAdmit(state.remoteAddress);
436
+ if (!authAdmit.ok) {
437
+ _emit("mail.server.pop3.auth_rate_limit_refused",
438
+ { connectionId: state.id, remoteAddress: state.remoteAddress, reason: authAdmit.reason },
439
+ "denied");
440
+ _writeErr(socket, "Too many AUTH failures from your IP");
441
+ _close(socket);
442
+ return;
443
+ }
444
+ var username = state.tentativeUser;
445
+ state.tentativeUser = null;
446
+ var password = args[0];
447
+ _emit("mail.server.pop3.auth_attempt",
448
+ { connectionId: state.id, verb: "PASS", remoteAddress: state.remoteAddress });
449
+ Promise.resolve()
450
+ .then(function () {
451
+ return authConfig.verify("PLAIN", {
452
+ username: username,
453
+ password: password,
454
+ tls: state.tls,
455
+ remoteAddress: state.remoteAddress,
456
+ });
457
+ })
458
+ .then(function (result) {
459
+ if (result && result.ok && result.actor) {
460
+ state.actor = result.actor;
461
+ _enterTransaction(state, socket, "PASS");
462
+ return;
463
+ }
464
+ rateLimit.noteAuthFailure(state.remoteAddress);
465
+ _emit("mail.server.pop3.auth_failed",
466
+ { connectionId: state.id, verb: "PASS", reason: "verify-returned-fail" }, "denied");
467
+ _writeErr(socket, "Authentication failed");
468
+ })
469
+ .catch(function () {
470
+ rateLimit.noteAuthFailure(state.remoteAddress);
471
+ _writeErr(socket, "Authentication failed");
472
+ });
473
+ }
474
+
475
+ function _handleApop(state, socket, args) {
476
+ // The validator already refuses APOP under strict; this just
477
+ // means the operator opted into balanced/permissive. Treat as
478
+ // username+digest and delegate to authConfig.verify with the APOP
479
+ // mechanism name.
480
+ if (state.stage !== "authorization") {
481
+ _writeErr(socket, "APOP only valid in AUTHORIZATION");
482
+ return;
483
+ }
484
+ if (!authConfig || typeof authConfig.verify !== "function") {
485
+ _writeErr(socket, "AUTH not configured");
486
+ return;
487
+ }
488
+ var authAdmit = rateLimit.checkAuthAdmit(state.remoteAddress);
489
+ if (!authAdmit.ok) {
490
+ _writeErr(socket, "Too many AUTH failures from your IP");
491
+ _close(socket);
492
+ return;
493
+ }
494
+ Promise.resolve()
495
+ .then(function () {
496
+ return authConfig.verify("APOP", {
497
+ username: args[0],
498
+ digest: args[1],
499
+ tls: state.tls,
500
+ remoteAddress: state.remoteAddress,
501
+ });
502
+ })
503
+ .then(function (result) {
504
+ if (result && result.ok && result.actor) {
505
+ state.actor = result.actor;
506
+ _enterTransaction(state, socket, "APOP");
507
+ return;
508
+ }
509
+ rateLimit.noteAuthFailure(state.remoteAddress);
510
+ _writeErr(socket, "Authentication failed");
511
+ })
512
+ .catch(function () {
513
+ rateLimit.noteAuthFailure(state.remoteAddress);
514
+ _writeErr(socket, "Authentication failed");
515
+ });
516
+ }
517
+
518
+ function _handleAuth(state, socket, args) {
519
+ if (state.stage !== "authorization") {
520
+ _writeErr(socket, "AUTH only valid in AUTHORIZATION");
521
+ return;
522
+ }
523
+ if (!authConfig || typeof authConfig.verify !== "function") {
524
+ _writeErr(socket, "AUTH not configured");
525
+ return;
526
+ }
527
+ if (args.length === 0) {
528
+ // RFC 5034 — `AUTH` alone enumerates mechanisms
529
+ _writeOk(socket, "Supported mechanisms follow");
530
+ var mechs = (authConfig.mechanisms || ["PLAIN"]).map(function (m) {
531
+ return String(m).toUpperCase();
532
+ });
533
+ for (var i = 0; i < mechs.length; i += 1) socket.write(mechs[i] + "\r\n");
534
+ socket.write(".\r\n");
535
+ return;
536
+ }
537
+ // RFC 2595 §2.1 + RFC 5034 §4 — refuse mech-bearing AUTH over
538
+ // cleartext under strict (defense-in-depth — guardPop3Command
539
+ // refuses at the validate boundary, this catches any
540
+ // configuration where the gate was relaxed but the AUTH path
541
+ // still receives traffic).
542
+ if (!state.tls && profile === "strict") {
543
+ _emit("mail.server.pop3.auth_refused_cleartext",
544
+ { connectionId: state.id, verb: "AUTH", mech: args[0] }, "denied");
545
+ _writeErr(socket, "AUTH refused over cleartext (use STLS first; RFC 2595 §2.1)");
546
+ return;
547
+ }
548
+ var authAdmit = rateLimit.checkAuthAdmit(state.remoteAddress);
549
+ if (!authAdmit.ok) {
550
+ _writeErr(socket, "Too many AUTH failures from your IP");
551
+ _close(socket);
552
+ return;
553
+ }
554
+ var mech = args[0].toUpperCase();
555
+ var initialResp = args.length > 1 ? args.slice(1).join(" ") : null;
556
+ Promise.resolve()
557
+ .then(function () {
558
+ return authConfig.verify(mech, {
559
+ clientResponse: initialResp,
560
+ tls: state.tls,
561
+ remoteAddress: state.remoteAddress,
562
+ });
563
+ })
564
+ .then(function (result) {
565
+ if (result && result.ok && result.actor) {
566
+ state.actor = result.actor;
567
+ _enterTransaction(state, socket, "AUTH/" + mech);
568
+ return;
569
+ }
570
+ rateLimit.noteAuthFailure(state.remoteAddress);
571
+ _writeErr(socket, "Authentication failed");
572
+ })
573
+ .catch(function () {
574
+ rateLimit.noteAuthFailure(state.remoteAddress);
575
+ _writeErr(socket, "Authentication failed");
576
+ });
577
+ }
578
+
579
+ function _enterTransaction(state, socket, verb) {
580
+ if (typeof mailStore.openPop3Drop !== "function") {
581
+ _writeErr(socket, "Backend missing openPop3Drop");
582
+ return;
583
+ }
584
+ Promise.resolve()
585
+ .then(function () { return mailStore.openPop3Drop(state.actor, {}); })
586
+ .then(function (drop) {
587
+ state.dropId = drop && drop.dropId;
588
+ state.stage = "transaction";
589
+ _emit("mail.server.pop3.auth_success",
590
+ { connectionId: state.id, verb: verb, tenantId: state.actor.tenantId || null });
591
+ _emit("mail.server.pop3.transaction_start",
592
+ { connectionId: state.id, dropCount: (drop && drop.count) || 0,
593
+ totalBytes: (drop && drop.totalBytes) || 0 });
594
+ _writeOk(socket, "Logged in");
595
+ })
596
+ .catch(function (err) {
597
+ _writeErr(socket, "Cannot open drop: " + ((err && err.message) || "backend error").slice(0, ERR_CLAMP));
598
+ });
599
+ }
600
+
601
+ function _handleQuit(state, socket) {
602
+ if (state.stage !== "transaction") {
603
+ _writeOk(socket, "Goodbye");
604
+ _close(socket);
605
+ return;
606
+ }
607
+ state.stage = "update";
608
+ Promise.resolve()
609
+ .then(function () { return mailStore.commitPop3Drop(state.actor, state.dropId); })
610
+ .then(function (info) {
611
+ _emit("mail.server.pop3.update_commit",
612
+ { connectionId: state.id, deleted: (info && info.deleted) || 0 });
613
+ _writeOk(socket, "Goodbye");
614
+ _close(socket);
615
+ })
616
+ .catch(function (err) {
617
+ _writeErr(socket, "Commit failed: " + ((err && err.message) || "backend error").slice(0, ERR_CLAMP));
618
+ _close(socket);
619
+ });
620
+ }
621
+
622
+ function _requireTrans(state, socket) {
623
+ if (state.stage !== "transaction") {
624
+ _writeErr(socket, "Not authorized; USER+PASS first");
625
+ return false;
626
+ }
627
+ return true;
628
+ }
629
+
630
+ function _handleStat(state, socket) {
631
+ if (!_requireTrans(state, socket)) return;
632
+ Promise.resolve()
633
+ .then(function () { return mailStore.listMessages(state.actor, state.dropId); })
634
+ .then(function (msgs) {
635
+ var ms = msgs || [];
636
+ var totalBytes = 0;
637
+ for (var i = 0; i < ms.length; i += 1) totalBytes += ms[i].size || 0;
638
+ _writeOk(socket, ms.length + " " + totalBytes);
639
+ })
640
+ .catch(function (err) { _writeErr(socket, ((err && err.message) || "stat failed").slice(0, ERR_CLAMP)); });
641
+ }
642
+
643
+ function _handleList(state, socket, args) {
644
+ if (!_requireTrans(state, socket)) return;
645
+ Promise.resolve()
646
+ .then(function () { return mailStore.listMessages(state.actor, state.dropId); })
647
+ .then(function (msgs) {
648
+ var ms = msgs || [];
649
+ if (args.length === 1) {
650
+ var n = parseInt(args[0], 10);
651
+ var found = null;
652
+ for (var i = 0; i < ms.length; i += 1) {
653
+ if (ms[i].msgNum === n) { found = ms[i]; break; }
654
+ }
655
+ if (!found) { _writeErr(socket, "no such message"); return; }
656
+ _writeOk(socket, n + " " + found.size);
657
+ return;
658
+ }
659
+ _writeOk(socket, ms.length + " messages");
660
+ for (var j = 0; j < ms.length; j += 1) {
661
+ socket.write(ms[j].msgNum + " " + ms[j].size + "\r\n");
662
+ }
663
+ socket.write(".\r\n");
664
+ })
665
+ .catch(function (err) { _writeErr(socket, ((err && err.message) || "list failed").slice(0, ERR_CLAMP)); });
666
+ }
667
+
668
+ function _handleRetr(state, socket, args) {
669
+ if (!_requireTrans(state, socket)) return;
670
+ var msgNum = parseInt(args[0], 10);
671
+ Promise.resolve()
672
+ .then(function () { return mailStore.getMessage(state.actor, state.dropId, msgNum, {}); })
673
+ .then(function (msg) {
674
+ if (!msg) { _writeErr(socket, "no such message"); return; }
675
+ _emit("mail.server.pop3.retr",
676
+ { connectionId: state.id, msgNum: msgNum, size: msg.size });
677
+ _writeOk(socket, msg.size + " octets");
678
+ // RFC 1939 §3 dot-stuffing — lines starting with `.` get a
679
+ // doubled `.` so the receiver doesn't mistake them for the
680
+ // CRLF.CRLF terminator. The `/^\./gm` regex on a JS string
681
+ // treats bare LF as a line boundary (matches `\n.` and
682
+ // `\r\n.`), so a body containing a bare-LF line that starts
683
+ // with `.` gained spurious stuffing that didn't match the
684
+ // receiver's strict-CRLF parser. Route through safeSmtp.dotStuff
685
+ // which inspects the raw Buffer and only treats canonical
686
+ // \r\n as a line boundary (bare LF is left alone — the
687
+ // guardSmtpCommand.detectBodySmuggling layer catches bare-LF
688
+ // smuggling at the upstream parse).
689
+ var bodyBuf = msg.rawBytes
690
+ ? msg.rawBytes
691
+ : Buffer.from(msg.text || "", "utf8");
692
+ var stuffed = safeSmtp.dotStuff(bodyBuf);
693
+ socket.write(stuffed);
694
+ // RFC 1939 §3 requires a CRLF before the terminator. The body
695
+ // may already end with CRLF; write one only when it doesn't.
696
+ if (stuffed.length === 0 ||
697
+ stuffed[stuffed.length - 2] !== 0x0d /* CR */ ||
698
+ stuffed[stuffed.length - 1] !== 0x0a /* LF */) {
699
+ socket.write("\r\n");
700
+ }
701
+ socket.write(".\r\n");
702
+ })
703
+ .catch(function (err) { _writeErr(socket, ((err && err.message) || "retr failed").slice(0, ERR_CLAMP)); });
704
+ }
705
+
706
+ function _handleDele(state, socket, args) {
707
+ if (!_requireTrans(state, socket)) return;
708
+ var msgNum = parseInt(args[0], 10);
709
+ Promise.resolve()
710
+ .then(function () { return mailStore.markDelete(state.actor, state.dropId, msgNum); })
711
+ .then(function () {
712
+ _emit("mail.server.pop3.dele",
713
+ { connectionId: state.id, msgNum: msgNum });
714
+ _writeOk(socket, "marked deleted");
715
+ })
716
+ .catch(function (err) { _writeErr(socket, ((err && err.message) || "dele failed").slice(0, ERR_CLAMP)); });
717
+ }
718
+
719
+ function _handleRset(state, socket) {
720
+ if (!_requireTrans(state, socket)) return;
721
+ Promise.resolve()
722
+ .then(function () {
723
+ if (typeof mailStore.resetPop3Drop === "function") {
724
+ return mailStore.resetPop3Drop(state.actor, state.dropId);
725
+ }
726
+ })
727
+ .then(function () { _writeOk(socket, "delete marks cleared"); })
728
+ .catch(function (err) { _writeErr(socket, ((err && err.message) || "rset failed").slice(0, ERR_CLAMP)); });
729
+ }
730
+
731
+ function _handleTop(state, socket, args) {
732
+ if (!_requireTrans(state, socket)) return;
733
+ var msgNum = parseInt(args[0], 10);
734
+ var headerLines = parseInt(args[1], 10);
735
+ Promise.resolve()
736
+ .then(function () {
737
+ return mailStore.getMessage(state.actor, state.dropId, msgNum,
738
+ { headersOnly: true, headerLines: headerLines });
739
+ })
740
+ .then(function (msg) {
741
+ if (!msg) { _writeErr(socket, "no such message"); return; }
742
+ _writeOk(socket, "headers + " + headerLines + " body lines");
743
+ // MAIL-15 — see _handleRetr; same byte-level CRLF-aware
744
+ // dot-stuffing primitive for the TOP partial-body path.
745
+ var bodyBuf = msg.rawBytes
746
+ ? msg.rawBytes
747
+ : Buffer.from(msg.text || "", "utf8");
748
+ var stuffed = safeSmtp.dotStuff(bodyBuf);
749
+ socket.write(stuffed);
750
+ if (stuffed.length === 0 ||
751
+ stuffed[stuffed.length - 2] !== 0x0d /* CR */ ||
752
+ stuffed[stuffed.length - 1] !== 0x0a /* LF */) {
753
+ socket.write("\r\n");
754
+ }
755
+ socket.write(".\r\n");
756
+ })
757
+ .catch(function (err) { _writeErr(socket, ((err && err.message) || "top failed").slice(0, ERR_CLAMP)); });
758
+ }
759
+
760
+ function _handleUidl(state, socket, args) {
761
+ if (!_requireTrans(state, socket)) return;
762
+ Promise.resolve()
763
+ .then(function () { return mailStore.listMessages(state.actor, state.dropId); })
764
+ .then(function (msgs) {
765
+ var ms = msgs || [];
766
+ if (args.length === 1) {
767
+ var n = parseInt(args[0], 10);
768
+ var found = null;
769
+ for (var i = 0; i < ms.length; i += 1) {
770
+ if (ms[i].msgNum === n) { found = ms[i]; break; }
771
+ }
772
+ if (!found) { _writeErr(socket, "no such message"); return; }
773
+ _writeOk(socket, n + " " + (found.uid || found.uidl || ""));
774
+ return;
775
+ }
776
+ _writeOk(socket, "unique-id listing follows");
777
+ for (var j = 0; j < ms.length; j += 1) {
778
+ socket.write(ms[j].msgNum + " " + (ms[j].uid || ms[j].uidl || "") + "\r\n");
779
+ }
780
+ socket.write(".\r\n");
781
+ })
782
+ .catch(function (err) { _writeErr(socket, ((err && err.message) || "uidl failed").slice(0, ERR_CLAMP)); });
783
+ }
784
+
785
+ function _writeOk(socket, msg) { try { socket.write("+OK " + msg + "\r\n"); } catch (_e) { /* socket down */ } }
786
+ function _writeErr(socket, msg) { try { socket.write("-ERR " + msg + "\r\n"); } catch (_e) { /* socket down */ } }
787
+ function _close(socket) {
788
+ try { socket.end(); } catch (_e) { /* idempotent */ }
789
+ try { socket.destroy(); } catch (_e2) { /* idempotent */ }
790
+ connections.delete(socket);
791
+ }
792
+
793
+ // ---- Lifecycle ----------------------------------------------------------
794
+ async function listen(listenOpts) {
795
+ listenOpts = listenOpts || {};
796
+ if (listening) {
797
+ throw new MailServerPop3Error("mail-server-pop3/already-listening",
798
+ "listen: already listening");
799
+ }
800
+ var port = listenOpts.port === undefined ? 110 : listenOpts.port; // allow:raw-byte-literal — RFC 1939 POP3 port (IANA)
801
+ var address = listenOpts.address || "0.0.0.0";
802
+ tcpServer = net.createServer(function (socket) { _handleConnection(socket); });
803
+ return new Promise(function (resolve, reject) {
804
+ tcpServer.once("error", reject);
805
+ tcpServer.listen(port, address, function () {
806
+ listening = true;
807
+ tcpServer.removeListener("error", reject);
808
+ _emit("mail.server.pop3.listening", { port: port, address: address });
809
+ resolve({ port: tcpServer.address().port, address: address });
810
+ });
811
+ });
812
+ }
813
+
814
+ async function close() {
815
+ if (!listening) return;
816
+ listening = false;
817
+ for (var s of connections) { try { s.destroy(); } catch (_e) { /* idempotent */ } }
818
+ connections.clear();
819
+ return new Promise(function (resolve) {
820
+ tcpServer.close(function () {
821
+ _emit("mail.server.pop3.closed", {});
822
+ resolve();
823
+ });
824
+ });
825
+ }
826
+
827
+ return {
828
+ listen: listen,
829
+ close: close,
830
+ };
831
+ }
832
+
833
+ module.exports = {
834
+ create: create,
835
+ MailServerPop3Error: MailServerPop3Error,
836
+ };