@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.
- package/CHANGELOG.md +952 -908
- package/index.js +25 -0
- package/lib/_test/crypto-fixtures.js +67 -0
- package/lib/agent-event-bus.js +52 -6
- package/lib/agent-idempotency.js +169 -16
- package/lib/agent-orchestrator.js +263 -9
- package/lib/agent-posture-chain.js +163 -5
- package/lib/agent-saga.js +146 -16
- package/lib/agent-snapshot.js +349 -19
- package/lib/agent-stream.js +34 -2
- package/lib/agent-tenant.js +179 -23
- package/lib/agent-trace.js +84 -21
- package/lib/auth/aal.js +8 -1
- package/lib/auth/ciba.js +6 -1
- package/lib/auth/dpop.js +7 -2
- package/lib/auth/fal.js +17 -8
- package/lib/auth/jwt-external.js +128 -4
- package/lib/auth/oauth.js +232 -10
- package/lib/auth/oid4vci.js +67 -7
- package/lib/auth/openid-federation.js +71 -25
- package/lib/auth/passkey.js +140 -6
- package/lib/auth/sd-jwt-vc.js +78 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/cli.js +13 -0
- package/lib/compliance.js +176 -8
- package/lib/crypto-field.js +114 -14
- package/lib/crypto.js +216 -20
- package/lib/db.js +1 -0
- package/lib/guard-graphql.js +37 -0
- package/lib/guard-jmap.js +321 -0
- package/lib/guard-managesieve-command.js +566 -0
- package/lib/guard-pop3-command.js +317 -0
- package/lib/guard-regex.js +138 -1
- package/lib/guard-smtp-command.js +58 -3
- package/lib/guard-xml.js +39 -1
- package/lib/mail-agent.js +20 -7
- package/lib/mail-arc-sign.js +12 -8
- package/lib/mail-auth.js +323 -34
- package/lib/mail-crypto-pgp.js +934 -0
- package/lib/mail-crypto-smime.js +340 -0
- package/lib/mail-crypto.js +108 -0
- package/lib/mail-dav.js +1224 -0
- package/lib/mail-deploy.js +492 -0
- package/lib/mail-dkim.js +431 -26
- package/lib/mail-journal.js +435 -0
- package/lib/mail-scan.js +502 -0
- package/lib/mail-server-imap.js +64 -26
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +40 -30
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +13 -0
- package/lib/mail-server-submission.js +70 -24
- package/lib/mail-server-tls.js +445 -0
- package/lib/mail-sieve.js +557 -0
- package/lib/mail-spam-score.js +284 -0
- package/lib/mail.js +99 -0
- package/lib/metrics.js +80 -3
- package/lib/middleware/dpop.js +58 -3
- package/lib/middleware/idempotency-key.js +255 -42
- package/lib/middleware/protected-resource-metadata.js +114 -2
- package/lib/network-dns-resolver.js +33 -0
- package/lib/network-tls.js +46 -0
- package/lib/otel-export.js +13 -4
- package/lib/outbox.js +62 -12
- package/lib/pqc-agent.js +13 -5
- package/lib/retry.js +23 -9
- package/lib/router.js +23 -1
- package/lib/safe-ical.js +634 -0
- package/lib/safe-icap.js +502 -0
- package/lib/safe-mime.js +15 -0
- package/lib/safe-sieve.js +684 -0
- package/lib/safe-smtp.js +57 -0
- package/lib/safe-url.js +37 -0
- package/lib/safe-vcard.js +473 -0
- package/lib/self-update-standalone-verifier.js +32 -3
- package/lib/self-update.js +153 -33
- package/lib/vendor/MANIFEST.json +161 -156
- package/lib/vendor-data.js +127 -9
- package/lib/vex.js +324 -59
- package/package.json +1 -1
- 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
|
+
};
|