@blamejs/core 0.9.46 → 0.10.1
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 +951 -893
- package/index.js +30 -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 +67 -5
- package/lib/circuit-breaker.js +10 -2
- 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-imap-command.js +335 -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-smtp-command.js +58 -3
- 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 +1102 -0
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +164 -34
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +269 -0
- package/lib/mail-server-submission.js +1032 -0
- 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 +130 -10
- 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/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 +168 -17
- 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,1102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.mail.server.imap
|
|
4
|
+
* @nav Mail
|
|
5
|
+
* @title Mail IMAP Server
|
|
6
|
+
* @order 546
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* IMAP4rev2 mailbox-access listener (RFC 9051; obsoletes RFC 3501).
|
|
10
|
+
* Modern MUAs (Thunderbird, Apple Mail, mutt, K-9, FairEmail,
|
|
11
|
+
* etc.) connect here to read + manage messages without operators
|
|
12
|
+
* running dovecot / cyrus alongside. Composes the framework's
|
|
13
|
+
* existing substrates:
|
|
14
|
+
*
|
|
15
|
+
* - `b.guardImapCommand` for wire-protocol shape + smuggling
|
|
16
|
+
* defense (literal-injection, bare-CR/LF refusal, per-verb
|
|
17
|
+
* shape, RFC 9051 §2.2.2 literal framing)
|
|
18
|
+
* - `b.mail.server.rateLimit` for per-IP DoS defense (concurrent
|
|
19
|
+
* + rate + AUTH-failure budget + slow-loris)
|
|
20
|
+
* - `b.mailStore` (operator-supplied backend) for the actual
|
|
21
|
+
* mail storage + UIDVALIDITY + modseq tracking
|
|
22
|
+
* - operator-supplied authenticator for SASL credential verify
|
|
23
|
+
* - `b.mail.server.tls` recommended for cert + key loading +
|
|
24
|
+
* rotation
|
|
25
|
+
*
|
|
26
|
+
* ## State machine (RFC 9051 §3)
|
|
27
|
+
*
|
|
28
|
+
* ```
|
|
29
|
+
* NOT-AUTHENTICATED → [STARTTLS → NOT-AUTH-TLS] → AUTH/LOGIN →
|
|
30
|
+
* AUTHENTICATED ↔ SELECTED → LOGOUT
|
|
31
|
+
* ↑ EXAMINE ↓ CLOSE / UNSELECT
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* Commands gated by state:
|
|
35
|
+
*
|
|
36
|
+
* - NOT-AUTHENTICATED: STARTTLS / AUTHENTICATE / LOGIN / NOOP /
|
|
37
|
+
* CAPABILITY / LOGOUT / ID
|
|
38
|
+
* - AUTHENTICATED: SELECT / EXAMINE / CREATE / DELETE / RENAME /
|
|
39
|
+
* SUBSCRIBE / UNSUBSCRIBE / LIST / STATUS / APPEND / NAMESPACE /
|
|
40
|
+
* IDLE / ENABLE / NOOP / CAPABILITY / LOGOUT / ID
|
|
41
|
+
* - SELECTED: CHECK / CLOSE / UNSELECT / EXPUNGE / SEARCH / FETCH /
|
|
42
|
+
* STORE / COPY / MOVE / UID … / IDLE / NOOP / CAPABILITY /
|
|
43
|
+
* LOGOUT + every AUTHENTICATED command
|
|
44
|
+
*
|
|
45
|
+
* Tagged response model: every client command carries a tag
|
|
46
|
+
* (`A001 LOGIN …`); server replies with one or more untagged
|
|
47
|
+
* responses (`* …`) then `A001 OK …` / `A001 NO …` / `A001 BAD …`.
|
|
48
|
+
*
|
|
49
|
+
* ## Wire-protocol defenses
|
|
50
|
+
*
|
|
51
|
+
* - **STARTTLS stripping (CVE-2021-33515 Dovecot class)** —
|
|
52
|
+
* STARTTLS upgrade clears pre-handshake receive buffer; any
|
|
53
|
+
* pipelined command queued before TLS is refused with
|
|
54
|
+
* `BAD Pipelined post-STARTTLS not permitted`.
|
|
55
|
+
*
|
|
56
|
+
* - **Literal-injection (CVE-2018-19518 INC IMAP class)** —
|
|
57
|
+
* `{n}` literal continuation MUST come on a line of its own
|
|
58
|
+
* (per `b.guardImapCommand.detectLiteralSmuggling`); oversize
|
|
59
|
+
* literals refused (default 64 MiB); LITERAL+ (RFC 7888) non-
|
|
60
|
+
* synchronizing literals only honored post-AUTH.
|
|
61
|
+
*
|
|
62
|
+
* - **Mailbox-name traversal** — mailbox path components
|
|
63
|
+
* validated through `_validateMailboxName`: refuses `..`, NUL,
|
|
64
|
+
* control chars, oversize. UTF-8 mailbox names (RFC 9051 §5.1)
|
|
65
|
+
* accepted; modified-UTF7 (RFC 3501 §5.1.3 legacy) refused unless
|
|
66
|
+
* `allowLegacyMUtf7: true`.
|
|
67
|
+
*
|
|
68
|
+
* - **APPEND-flood** — per-tenant byte/sec cap surfaces via the
|
|
69
|
+
* `b.mail.server.rateLimit`'s `minBytesPerSecond` floor on the
|
|
70
|
+
* APPEND-literal-body phase (same shape the MX listener uses for
|
|
71
|
+
* DATA-body).
|
|
72
|
+
*
|
|
73
|
+
* - **Resource exhaustion** — per-line cap (default 8 KiB sans
|
|
74
|
+
* literal payload), per-literal cap (64 MiB), per-connection idle
|
|
75
|
+
* cap (default 30 min when not in IDLE; IDLE itself capped at
|
|
76
|
+
* 29 min per RFC 2177 §3 to force re-issue).
|
|
77
|
+
*
|
|
78
|
+
* - **Connection-rate + AUTH-failure budget** — composes
|
|
79
|
+
* `b.mail.server.rateLimit`. Each AUTH failure increments the
|
|
80
|
+
* budget; trip the cap and new AUTH attempts get
|
|
81
|
+
* `* BAD Too many AUTH failures` + connection close.
|
|
82
|
+
*
|
|
83
|
+
* ## Audit lifecycle
|
|
84
|
+
*
|
|
85
|
+
* - `mail.server.imap.connect` — IP, TLS state
|
|
86
|
+
* - `mail.server.imap.auth_attempt` — mechanism, actor-hash
|
|
87
|
+
* - `mail.server.imap.auth_success` — mechanism, tenantId, scopes
|
|
88
|
+
* - `mail.server.imap.auth_failed` — mechanism, reason
|
|
89
|
+
* - `mail.server.imap.select` — mailbox, modseq, exists count
|
|
90
|
+
* - `mail.server.imap.append` — mailbox, size, flags
|
|
91
|
+
* - `mail.server.imap.fetch_bulk` — sequence-set size, BODY parts
|
|
92
|
+
* - `mail.server.imap.expunge` — count, modseq
|
|
93
|
+
* - `mail.server.imap.literal_overflow_refused` — attempt size, cap
|
|
94
|
+
* - `mail.server.imap.rate_limit_refused` — IP, reason
|
|
95
|
+
* - `mail.server.imap.smtp_smuggling_detected` — literal-injection
|
|
96
|
+
*
|
|
97
|
+
* ## What v1 does NOT ship
|
|
98
|
+
*
|
|
99
|
+
* - **SEARCH** — operator wires `opts.search(actor, mailbox, query)`
|
|
100
|
+
* when ready; the listener emits `BAD search-not-configured`
|
|
101
|
+
* until then. SEARCH expressions are operator-domain logic
|
|
102
|
+
* against the mailStore index.
|
|
103
|
+
* - **NOTIFY (RFC 5465)**, **METADATA (RFC 5464)**, **CATENATE
|
|
104
|
+
* (RFC 4469)**, **URLAUTH (RFC 4467)**, **IMAPSIEVE (RFC 6785)**,
|
|
105
|
+
* **COMPRESS=DEFLATE (RFC 4978)** — opt-in / refused.
|
|
106
|
+
* - **CONDSTORE / QRESYNC (RFC 7162)** — modseq is exposed via
|
|
107
|
+
* STATUS but per-FETCH CHANGEDSINCE delta is operator-side
|
|
108
|
+
* follow-up.
|
|
109
|
+
*
|
|
110
|
+
* @card
|
|
111
|
+
* IMAP4rev2 mailbox-access listener (RFC 9051; obsoletes RFC 3501).
|
|
112
|
+
* State machine NOT-AUTH → STARTTLS → AUTH → SELECTED → LOGOUT.
|
|
113
|
+
* Composes b.guardImapCommand (wire-protocol gate), b.mail.server.
|
|
114
|
+
* rateLimit (DoS defense), operator-supplied mailStore + SASL
|
|
115
|
+
* authenticator. Default-on per-IP rate-limit + literal-injection
|
|
116
|
+
* refusal + mailbox-traversal refusal.
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
var net = require("node:net");
|
|
120
|
+
var lazyRequire = require("./lazy-require");
|
|
121
|
+
var C = require("./constants");
|
|
122
|
+
var bCrypto = require("./crypto");
|
|
123
|
+
var numericBounds = require("./numeric-bounds");
|
|
124
|
+
var validateOpts = require("./validate-opts");
|
|
125
|
+
var guardImapCommand = require("./guard-imap-command");
|
|
126
|
+
var mailServerRateLimit = require("./mail-server-rate-limit");
|
|
127
|
+
var mailServerTls = require("./mail-server-tls");
|
|
128
|
+
var { defineClass } = require("./framework-error");
|
|
129
|
+
|
|
130
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
131
|
+
|
|
132
|
+
var MailServerImapError = defineClass("MailServerImapError", { alwaysPermanent: true });
|
|
133
|
+
|
|
134
|
+
var DEFAULT_MAX_LINE_BYTES = C.BYTES.kib(8);
|
|
135
|
+
var DEFAULT_MAX_LITERAL = C.BYTES.mib(64);
|
|
136
|
+
var DEFAULT_IDLE_TIMEOUT_MS = C.TIME.minutes(30);
|
|
137
|
+
var IDLE_BANDWIDTH_TIMEOUT_MS = C.TIME.minutes(29); // RFC 2177 §3 — re-issue before 30
|
|
138
|
+
var DEFAULT_GREETING_VENDOR = "blamejs IMAP4rev2";
|
|
139
|
+
var pkgVersion = require("../package.json").version;
|
|
140
|
+
|
|
141
|
+
// Error-message clamp bytes — protocol-string clamp, not a byte count.
|
|
142
|
+
// Centralized so the allow:raw-byte-literal marker lives in one place
|
|
143
|
+
// and the per-call sites read cleanly.
|
|
144
|
+
var ERR_CLAMP = 200; // allow:raw-byte-literal — protocol-reply error-message clamp
|
|
145
|
+
var LINE_PREVIEW = 80; // allow:raw-byte-literal — audit-line preview clamp
|
|
146
|
+
|
|
147
|
+
// Mailbox name validator. RFC 9051 §5.1 — UTF-8 hierarchy. Refuse
|
|
148
|
+
// path-traversal (`..`), NUL, C0 controls, leading/trailing slash,
|
|
149
|
+
// oversize.
|
|
150
|
+
function _validateMailboxName(name, opts) {
|
|
151
|
+
if (typeof name !== "string" || name.length === 0) return false;
|
|
152
|
+
if (name.length > 1024) return false; // allow:raw-byte-literal — mailbox name cap
|
|
153
|
+
for (var i = 0; i < name.length; i += 1) {
|
|
154
|
+
var c = name.charCodeAt(i);
|
|
155
|
+
if (c < 0x20 || c === 0x7F) return false; // allow:raw-byte-literal — control-byte refusal
|
|
156
|
+
}
|
|
157
|
+
if (name.indexOf("..") !== -1) return false;
|
|
158
|
+
if (name === "/" || name[0] === "/" || name[name.length - 1] === "/") return false;
|
|
159
|
+
// Modified-UTF7 detection — RFC 3501 §5.1.3. Sequences are
|
|
160
|
+
// `&...-`. Refuse under strict (RFC 9051 uses raw UTF-8).
|
|
161
|
+
if (opts && opts.allowLegacyMUtf7 !== true) {
|
|
162
|
+
if (/&[A-Za-z0-9+/]*-/.test(name)) return false; // allow:regex-no-length-cap — mailbox name already length-capped above
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @primitive b.mail.server.imap.create
|
|
169
|
+
* @signature b.mail.server.imap.create(opts)
|
|
170
|
+
* @since 0.9.49
|
|
171
|
+
* @status stable
|
|
172
|
+
* @related b.mail.server.mx.create, b.mail.server.submission.create, b.mailStore.create
|
|
173
|
+
*
|
|
174
|
+
* Build an IMAP4rev2 listener (RFC 9051). The handle exposes
|
|
175
|
+
* `listen({ port, address })` → ephemeral-bind promise resolving to
|
|
176
|
+
* `{ port, address }`, plus `close()` for graceful shutdown.
|
|
177
|
+
*
|
|
178
|
+
* @opts
|
|
179
|
+
* tlsContext: SecureContext, // required (no plaintext mode)
|
|
180
|
+
* greeting: string, // default "blamejs IMAP4rev2"
|
|
181
|
+
* maxLineBytes: number, // default 8192
|
|
182
|
+
* maxLiteralBytes: number, // default 64 MiB
|
|
183
|
+
* idleTimeoutMs: number, // default 30 min
|
|
184
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
185
|
+
* auth: {
|
|
186
|
+
* mechanisms: ["PLAIN", "LOGIN", "SCRAM-SHA-256", "EXTERNAL", "XOAUTH2"],
|
|
187
|
+
* verify: async function (mechanism, credentials) → { ok, actor },
|
|
188
|
+
* },
|
|
189
|
+
* mailStore: b.mailStore handle, // required
|
|
190
|
+
* rateLimit: b.mail.server.rateLimit handle | opts | false,
|
|
191
|
+
* audit: b.audit // optional
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* var imap = b.mail.server.imap.create({
|
|
195
|
+
* tlsContext: b.mail.server.tls.context({ certFile, keyFile }).secureContext,
|
|
196
|
+
* auth: {
|
|
197
|
+
* mechanisms: ["PLAIN", "SCRAM-SHA-256"],
|
|
198
|
+
* verify: async function (mech, creds) {
|
|
199
|
+
* return { ok: true, actor: { tenantId: "t1", username: creds.authzid } };
|
|
200
|
+
* },
|
|
201
|
+
* },
|
|
202
|
+
* mailStore: b.mailStore.create({ backend: b.db.handle() }),
|
|
203
|
+
* });
|
|
204
|
+
* await imap.listen({ port: 143 });
|
|
205
|
+
*/
|
|
206
|
+
function create(opts) {
|
|
207
|
+
validateOpts.requireObject(opts, "mail.server.imap.create",
|
|
208
|
+
MailServerImapError, "mail-server-imap/bad-opts");
|
|
209
|
+
if (!opts.tlsContext) {
|
|
210
|
+
throw new MailServerImapError("mail-server-imap/no-tls-context",
|
|
211
|
+
"mail.server.imap.create: tlsContext is required (no implicit plaintext mode). " +
|
|
212
|
+
"Use b.mail.server.tls.context({ certFile, keyFile, watch: true }) to load + " +
|
|
213
|
+
"auto-reload a cert/key pair from disk.");
|
|
214
|
+
}
|
|
215
|
+
if (!opts.mailStore || typeof opts.mailStore.appendMessage !== "function") {
|
|
216
|
+
throw new MailServerImapError("mail-server-imap/no-mail-store",
|
|
217
|
+
"mail.server.imap.create: mailStore is required (compose b.mailStore.create({ backend: ... }))");
|
|
218
|
+
}
|
|
219
|
+
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
220
|
+
["maxLineBytes", "maxLiteralBytes", "idleTimeoutMs"],
|
|
221
|
+
"mail.server.imap.", MailServerImapError, "mail-server-imap/bad-bound");
|
|
222
|
+
|
|
223
|
+
var greeting = opts.greeting || DEFAULT_GREETING_VENDOR;
|
|
224
|
+
var maxLineBytes = opts.maxLineBytes || DEFAULT_MAX_LINE_BYTES;
|
|
225
|
+
var maxLiteralBytes = opts.maxLiteralBytes || DEFAULT_MAX_LITERAL;
|
|
226
|
+
var idleTimeoutMs = opts.idleTimeoutMs || DEFAULT_IDLE_TIMEOUT_MS;
|
|
227
|
+
var profile = opts.profile || "strict";
|
|
228
|
+
var authConfig = opts.auth || null;
|
|
229
|
+
var mailStore = opts.mailStore;
|
|
230
|
+
var allowLegacyMUtf7 = profile === "permissive";
|
|
231
|
+
|
|
232
|
+
var rateLimit;
|
|
233
|
+
if (opts.rateLimit === false) {
|
|
234
|
+
rateLimit = mailServerRateLimit.create({ disabled: true });
|
|
235
|
+
} else if (opts.rateLimit && typeof opts.rateLimit.admitConnection === "function") {
|
|
236
|
+
rateLimit = opts.rateLimit;
|
|
237
|
+
} else {
|
|
238
|
+
rateLimit = mailServerRateLimit.create(opts.rateLimit || {});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
var tcpServer = null;
|
|
242
|
+
var listening = false;
|
|
243
|
+
var connections = new Set();
|
|
244
|
+
|
|
245
|
+
function _emit(action, metadata, outcome) {
|
|
246
|
+
try {
|
|
247
|
+
audit().safeEmit({
|
|
248
|
+
action: action,
|
|
249
|
+
outcome: outcome || "success",
|
|
250
|
+
metadata: metadata || {},
|
|
251
|
+
});
|
|
252
|
+
} catch (_e) { /* drop-silent — audit best-effort */ }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function _handleConnection(rawSocket) {
|
|
256
|
+
var remoteAddress = rawSocket.remoteAddress || "0.0.0.0";
|
|
257
|
+
var admit = rateLimit.admitConnection(remoteAddress);
|
|
258
|
+
if (!admit.ok) {
|
|
259
|
+
_emit("mail.server.imap.rate_limit_refused",
|
|
260
|
+
{ remoteAddress: remoteAddress, reason: admit.reason }, "denied");
|
|
261
|
+
try { rawSocket.write("* BAD Too many connections from your IP\r\n"); }
|
|
262
|
+
catch (_e) { /* socket may be down */ }
|
|
263
|
+
try { rawSocket.destroy(); } catch (_e2) { /* idempotent */ }
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
rawSocket.once("close", function () { rateLimit.releaseConnection(remoteAddress); });
|
|
267
|
+
|
|
268
|
+
var connectionId = "imapconn-" + bCrypto.generateToken(8); // allow:raw-byte-literal — connection-id length
|
|
269
|
+
var socket = rawSocket;
|
|
270
|
+
connections.add(socket);
|
|
271
|
+
|
|
272
|
+
var state = {
|
|
273
|
+
id: connectionId,
|
|
274
|
+
remoteAddress: remoteAddress,
|
|
275
|
+
tls: false,
|
|
276
|
+
stage: "not-authenticated",
|
|
277
|
+
actor: null,
|
|
278
|
+
selectedMailbox: null,
|
|
279
|
+
selectedReadOnly: false,
|
|
280
|
+
authPending: null,
|
|
281
|
+
pendingLiteral: null, // { tag, verb, line, size, body }
|
|
282
|
+
idle: null, // { tag, timer }
|
|
283
|
+
// Per-connection receive buffer (must NOT be a closure variable —
|
|
284
|
+
// multiple concurrent connections would clobber each other).
|
|
285
|
+
lineBuffer: Buffer.alloc(0),
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
_emit("mail.server.imap.connect",
|
|
289
|
+
{ connectionId: connectionId, remoteAddress: remoteAddress });
|
|
290
|
+
|
|
291
|
+
socket.setTimeout(idleTimeoutMs);
|
|
292
|
+
socket.on("timeout", function () {
|
|
293
|
+
_writeUntagged(socket, "BYE Idle timeout");
|
|
294
|
+
_close(socket, state);
|
|
295
|
+
});
|
|
296
|
+
socket.on("error", function (err) {
|
|
297
|
+
_emit("mail.server.imap.socket_error",
|
|
298
|
+
{ connectionId: connectionId, error: (err && err.message) || String(err) }, "failure");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Greeting per RFC 9051 §7.1.5 — `* OK <greeting>`.
|
|
302
|
+
_writeUntagged(socket, "OK [CAPABILITY " + _capabilityLine(state) + "] " + greeting);
|
|
303
|
+
|
|
304
|
+
socket.on("data", function (chunk) {
|
|
305
|
+
// Per-line cap MUST gate the concat — a single large TCP chunk
|
|
306
|
+
// (~64 KiB on most kernels) can push the buffer past the line
|
|
307
|
+
// cap BEFORE the drain loop runs, so the cap-check inside the
|
|
308
|
+
// loop sees a buffer that's already grown past the policy
|
|
309
|
+
// floor. When the chunk would itself overrun the line cap AND
|
|
310
|
+
// no literal is pending (where over-cap bytes are legitimate
|
|
311
|
+
// payload), reject here and tear the connection down.
|
|
312
|
+
var pendingLiteral = state.pendingLiteral;
|
|
313
|
+
var room = pendingLiteral
|
|
314
|
+
? (pendingLiteral.size - pendingLiteral.body.length) + maxLineBytes
|
|
315
|
+
: (maxLineBytes - state.lineBuffer.length);
|
|
316
|
+
if (chunk.length > room) {
|
|
317
|
+
_writeUntagged(socket, "BAD Line too long (cap " + maxLineBytes + ")");
|
|
318
|
+
_close(socket, state);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
state.lineBuffer = Buffer.concat([state.lineBuffer, chunk]);
|
|
322
|
+
_drainBuffer(state, socket);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Receive-buffer drain: extract complete lines (CRLF-terminated)
|
|
327
|
+
// and dispatch. When the previous command opened a literal (e.g.
|
|
328
|
+
// APPEND ... {N}), the next N bytes are the literal payload — we
|
|
329
|
+
// accumulate them before resuming line-mode dispatch.
|
|
330
|
+
function _drainBuffer(state, socket) {
|
|
331
|
+
while (true) {
|
|
332
|
+
if (state.pendingLiteral) {
|
|
333
|
+
var need = state.pendingLiteral.size - state.pendingLiteral.body.length;
|
|
334
|
+
if (state.lineBuffer.length < need) {
|
|
335
|
+
state.pendingLiteral.body = Buffer.concat([state.pendingLiteral.body, state.lineBuffer]);
|
|
336
|
+
state.lineBuffer = Buffer.alloc(0);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
state.pendingLiteral.body = Buffer.concat([state.pendingLiteral.body, state.lineBuffer.subarray(0, need)]);
|
|
340
|
+
state.lineBuffer = state.lineBuffer.subarray(need);
|
|
341
|
+
_completeLiteralCommand(state, socket);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
var crlf = state.lineBuffer.indexOf("\r\n");
|
|
345
|
+
if (crlf === -1) {
|
|
346
|
+
if (state.lineBuffer.length > maxLineBytes) {
|
|
347
|
+
_writeUntagged(socket, "BAD Line too long (cap " + maxLineBytes + ")");
|
|
348
|
+
_close(socket, state);
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
var rawLine = state.lineBuffer.subarray(0, crlf).toString("utf8");
|
|
353
|
+
state.lineBuffer = state.lineBuffer.subarray(crlf + 2);
|
|
354
|
+
_handleLine(state, socket, rawLine);
|
|
355
|
+
if (state.stage === "closed") return;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function _handleLine(state, socket, line) {
|
|
360
|
+
// Continuation: AUTHENTICATE multi-step expects a client response
|
|
361
|
+
if (state.authPending) {
|
|
362
|
+
_runAuthStep(state, socket, line.trim());
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// IDLE termination — RFC 2177 §3 expects `DONE` line.
|
|
366
|
+
if (state.idle) {
|
|
367
|
+
if (line.toUpperCase() === "DONE") {
|
|
368
|
+
var idleTag = state.idle.tag;
|
|
369
|
+
if (state.idle.timer) clearTimeout(state.idle.timer);
|
|
370
|
+
state.idle = null;
|
|
371
|
+
_writeTagged(socket, idleTag, "OK IDLE terminated");
|
|
372
|
+
} else {
|
|
373
|
+
_writeUntagged(socket, "BAD Expected DONE during IDLE");
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
var parsed;
|
|
378
|
+
try {
|
|
379
|
+
parsed = guardImapCommand.validate(line, {
|
|
380
|
+
profile: profile,
|
|
381
|
+
authenticated: state.actor !== null,
|
|
382
|
+
});
|
|
383
|
+
} catch (e) {
|
|
384
|
+
if (e && e.code === "guard-imap-command/literal-smuggling") {
|
|
385
|
+
_emit("mail.server.imap.smtp_smuggling_detected",
|
|
386
|
+
{ connectionId: state.id, line: line.slice(0, LINE_PREVIEW) }, "denied");
|
|
387
|
+
}
|
|
388
|
+
_writeUntagged(socket, "BAD " + (e && e.message ? e.message.slice(0, ERR_CLAMP) : "syntax"));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
// Literal-opener: stash + emit continuation. Zero-length literals
|
|
392
|
+
// (`{0}`) are legal per RFC 9051 §6.3.12 (e.g. APPEND of an empty
|
|
393
|
+
// message body — rare but spec-compliant; refusing them would
|
|
394
|
+
// diverge from the wire-protocol).
|
|
395
|
+
if (parsed.literalSize !== null) {
|
|
396
|
+
if (parsed.literalSize > maxLiteralBytes) {
|
|
397
|
+
_emit("mail.server.imap.literal_overflow_refused",
|
|
398
|
+
{ connectionId: state.id, attempted: parsed.literalSize, cap: maxLiteralBytes },
|
|
399
|
+
"denied");
|
|
400
|
+
_writeTagged(socket, parsed.tag,
|
|
401
|
+
"NO Literal " + parsed.literalSize + " bytes exceeds cap " + maxLiteralBytes);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
// Zero-byte literal: no continuation, no read — synthesize the
|
|
405
|
+
// pending-literal with an empty body and complete immediately on
|
|
406
|
+
// the next loop tick.
|
|
407
|
+
if (parsed.literalSize === 0) {
|
|
408
|
+
state.pendingLiteral = {
|
|
409
|
+
tag: parsed.tag,
|
|
410
|
+
verb: parsed.verb,
|
|
411
|
+
line: line,
|
|
412
|
+
size: 0,
|
|
413
|
+
body: Buffer.alloc(0),
|
|
414
|
+
synchronizing: !parsed.literalNonSync,
|
|
415
|
+
};
|
|
416
|
+
_completeLiteralCommand(state, socket);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
state.pendingLiteral = {
|
|
420
|
+
tag: parsed.tag,
|
|
421
|
+
verb: parsed.verb,
|
|
422
|
+
line: line,
|
|
423
|
+
size: parsed.literalSize,
|
|
424
|
+
body: Buffer.alloc(0),
|
|
425
|
+
synchronizing: !parsed.literalNonSync,
|
|
426
|
+
};
|
|
427
|
+
if (!parsed.literalNonSync) {
|
|
428
|
+
_writeUntagged(socket, "+ Ready for literal data");
|
|
429
|
+
}
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
_dispatch(state, socket, parsed, line);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function _completeLiteralCommand(state, socket) {
|
|
436
|
+
var pending = state.pendingLiteral;
|
|
437
|
+
state.pendingLiteral = null;
|
|
438
|
+
// Strip the trailing literal opener `{N}` (or `{N+}`) from the line
|
|
439
|
+
var lineNoLit = pending.line.replace(/\{[0-9]+\+?\}$/, "").trim(); // allow:regex-no-length-cap — line length already capped upstream
|
|
440
|
+
var parsed;
|
|
441
|
+
try { parsed = guardImapCommand.validate(lineNoLit, { profile: profile, authenticated: state.actor !== null }); }
|
|
442
|
+
catch (e) {
|
|
443
|
+
_writeTagged(socket, pending.tag, "BAD " + (e && e.message ? e.message.slice(0, ERR_CLAMP) : "syntax"));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
_dispatch(state, socket, parsed, lineNoLit, pending.body);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function _dispatch(state, socket, parsed, _rawLine, literalBody) {
|
|
450
|
+
var tag = parsed.tag;
|
|
451
|
+
var verb = parsed.verb;
|
|
452
|
+
var args = parsed.args;
|
|
453
|
+
|
|
454
|
+
switch (verb) {
|
|
455
|
+
case "CAPABILITY": return _handleCapability(state, socket, tag);
|
|
456
|
+
case "NOOP": return _writeTagged(socket, tag, "OK NOOP completed");
|
|
457
|
+
case "LOGOUT": return _handleLogout(state, socket, tag);
|
|
458
|
+
case "ID": return _handleId(state, socket, tag, args);
|
|
459
|
+
case "STARTTLS": return _handleStartTls(state, socket, tag);
|
|
460
|
+
case "AUTHENTICATE": return _handleAuthenticate(state, socket, tag, args);
|
|
461
|
+
case "LOGIN": return _handleLogin(state, socket, tag, args);
|
|
462
|
+
case "ENABLE": return _writeTagged(socket, tag, "OK ENABLED");
|
|
463
|
+
case "SELECT":
|
|
464
|
+
case "EXAMINE": return _handleSelect(state, socket, tag, args, verb === "EXAMINE");
|
|
465
|
+
case "LIST": return _handleList(state, socket, tag, args);
|
|
466
|
+
case "STATUS": return _handleStatus(state, socket, tag, args);
|
|
467
|
+
case "NAMESPACE": return _writeUntagged(socket, "NAMESPACE ((\"\" \"/\")) NIL NIL"), _writeTagged(socket, tag, "OK NAMESPACE completed");
|
|
468
|
+
case "APPEND": return _handleAppend(state, socket, tag, args, literalBody);
|
|
469
|
+
case "CHECK": return _writeTagged(socket, tag, "OK CHECK completed");
|
|
470
|
+
case "CLOSE":
|
|
471
|
+
case "UNSELECT": return _handleClose(state, socket, tag);
|
|
472
|
+
case "EXPUNGE": return _handleExpunge(state, socket, tag);
|
|
473
|
+
case "FETCH": return _handleFetch(state, socket, tag, args);
|
|
474
|
+
case "STORE": return _handleStore(state, socket, tag, args);
|
|
475
|
+
case "UID": return _handleUid(state, socket, tag, args);
|
|
476
|
+
case "IDLE": return _handleIdle(state, socket, tag);
|
|
477
|
+
case "DONE": return _writeTagged(socket, tag, "BAD DONE outside IDLE");
|
|
478
|
+
default: return _writeTagged(socket, tag, "BAD Verb '" + verb + "' not implemented in v1");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function _capabilityLine(state) {
|
|
483
|
+
var caps = ["IMAP4rev2"];
|
|
484
|
+
if (!state.tls) caps.push("STARTTLS");
|
|
485
|
+
// Advertise AUTH=<mech> ONLY for mechanisms the operator wired
|
|
486
|
+
// in opts.auth.mechanisms. RFC 9051 §7.2 — clients pick from the
|
|
487
|
+
// advertised list; advertising AUTH=PLAIN when authConfig is null
|
|
488
|
+
// or doesn't include PLAIN sets clients up for AUTHENTICATE
|
|
489
|
+
// requests that the listener refuses with "no AUTHENTICATE
|
|
490
|
+
// configured" / "mechanism not advertised".
|
|
491
|
+
if (authConfig && Array.isArray(authConfig.mechanisms)) {
|
|
492
|
+
for (var i = 0; i < authConfig.mechanisms.length; i += 1) {
|
|
493
|
+
var m = String(authConfig.mechanisms[i]).toUpperCase();
|
|
494
|
+
if (caps.indexOf("AUTH=" + m) === -1) caps.push("AUTH=" + m);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return caps.join(" ");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function _handleCapability(state, socket, tag) {
|
|
501
|
+
_writeUntagged(socket, "CAPABILITY " + _capabilityLine(state));
|
|
502
|
+
_writeTagged(socket, tag, "OK CAPABILITY completed");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function _handleId(state, socket, tag, args) {
|
|
506
|
+
// RFC 2971 — clients send a key/value list, server replies with
|
|
507
|
+
// its own. We accept anything (validator caps line size) and reply
|
|
508
|
+
// with a minimal identifier.
|
|
509
|
+
void args;
|
|
510
|
+
_writeUntagged(socket, "ID (\"name\" \"blamejs\" \"version\" \"" + pkgVersion + "\")");
|
|
511
|
+
_writeTagged(socket, tag, "OK ID completed");
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function _handleLogout(state, socket, tag) {
|
|
515
|
+
_writeUntagged(socket, "BYE Logging out");
|
|
516
|
+
_writeTagged(socket, tag, "OK LOGOUT completed");
|
|
517
|
+
_close(socket, state);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function _handleStartTls(state, socket, tag) {
|
|
521
|
+
if (state.tls) {
|
|
522
|
+
_writeTagged(socket, tag, "BAD TLS already negotiated");
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
_writeTagged(socket, tag, "OK Begin TLS negotiation now");
|
|
526
|
+
// Drain EVERY pre-handshake state field that could carry attacker-
|
|
527
|
+
// controlled bytes past the upgrade boundary (RFC 9051 §11.1 /
|
|
528
|
+
// CVE-2021-33515 class STARTTLS-injection defense):
|
|
529
|
+
// - lineBuffer: unparsed bytes pipelined before the handshake.
|
|
530
|
+
// - pendingLiteral: half-collected APPEND/AUTHENTICATE literal
|
|
531
|
+
// bytes; if not cleared, the literal completes after upgrade
|
|
532
|
+
// using bytes the peer sent in plaintext.
|
|
533
|
+
// - authPending: the AUTHENTICATE step token; a dangling token
|
|
534
|
+
// would let the post-TLS state machine resume an exchange that
|
|
535
|
+
// started in plaintext, conflating cleartext + TLS-protected
|
|
536
|
+
// phases of the same SASL run.
|
|
537
|
+
// Listener-removal + idle-timeout re-arm live in the shared
|
|
538
|
+
// upgradeSocket helper (b.mail.server.tls.upgradeSocket).
|
|
539
|
+
state.lineBuffer = Buffer.alloc(0);
|
|
540
|
+
state.pendingLiteral = null;
|
|
541
|
+
state.authPending = null;
|
|
542
|
+
mailServerTls.upgradeSocket({
|
|
543
|
+
plainSocket: socket,
|
|
544
|
+
secureContext: opts.tlsContext,
|
|
545
|
+
idleTimeoutMs: idleTimeoutMs,
|
|
546
|
+
onSecure: function (_tlsSocket) { state.tls = true; },
|
|
547
|
+
onData: function (tlsSocket, chunk) {
|
|
548
|
+
state.lineBuffer = Buffer.concat([state.lineBuffer, chunk]);
|
|
549
|
+
_drainBuffer(state, tlsSocket);
|
|
550
|
+
},
|
|
551
|
+
onError: function (err) {
|
|
552
|
+
_emit("mail.server.imap.tls_handshake_failed",
|
|
553
|
+
{ connectionId: state.id, error: (err && err.message) || String(err) }, "failure");
|
|
554
|
+
_close(socket, state);
|
|
555
|
+
},
|
|
556
|
+
onTimeout: function (tlsSocket) {
|
|
557
|
+
_writeUntagged(tlsSocket, "BYE Idle timeout");
|
|
558
|
+
_close(tlsSocket, state);
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function _handleAuthenticate(state, socket, tag, args) {
|
|
564
|
+
if (state.actor) {
|
|
565
|
+
_writeTagged(socket, tag, "BAD Already authenticated");
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (!state.tls && profile !== "permissive") {
|
|
569
|
+
_writeTagged(socket, tag, "BAD AUTHENTICATE requires TLS (use STARTTLS first)");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (!authConfig || typeof authConfig.verify !== "function") {
|
|
573
|
+
_writeTagged(socket, tag, "NO AUTHENTICATE not configured on this listener");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
var authAdmit = rateLimit.checkAuthAdmit(state.remoteAddress);
|
|
577
|
+
if (!authAdmit.ok) {
|
|
578
|
+
_emit("mail.server.imap.auth_rate_limit_refused",
|
|
579
|
+
{ connectionId: state.id, remoteAddress: state.remoteAddress, reason: authAdmit.reason },
|
|
580
|
+
"denied");
|
|
581
|
+
_writeTagged(socket, tag, "NO [ALERT] Too many AUTH failures from your IP");
|
|
582
|
+
_close(socket, state);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
var mechName = args.split(" ")[0].toUpperCase();
|
|
586
|
+
var initialResp = args.indexOf(" ") === -1 ? null : args.slice(args.indexOf(" ") + 1).trim();
|
|
587
|
+
var mechanisms = (authConfig.mechanisms || ["PLAIN", "LOGIN"]).map(function (m) {
|
|
588
|
+
return String(m).toUpperCase();
|
|
589
|
+
});
|
|
590
|
+
if (mechanisms.indexOf(mechName) === -1) {
|
|
591
|
+
_writeTagged(socket, tag, "NO Mechanism '" + mechName + "' not advertised");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
_emit("mail.server.imap.auth_attempt",
|
|
595
|
+
{ connectionId: state.id, mechanism: mechName, remoteAddress: state.remoteAddress });
|
|
596
|
+
state.authPending = { mechanism: mechName, tag: tag, step: 0 };
|
|
597
|
+
_runAuthStep(state, socket, initialResp);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function _runAuthStep(state, socket, clientResp) {
|
|
601
|
+
var pending = state.authPending;
|
|
602
|
+
Promise.resolve()
|
|
603
|
+
.then(function () {
|
|
604
|
+
return authConfig.verify(pending.mechanism, {
|
|
605
|
+
step: pending.step,
|
|
606
|
+
clientResponse: clientResp,
|
|
607
|
+
tls: state.tls,
|
|
608
|
+
remoteAddress: state.remoteAddress,
|
|
609
|
+
});
|
|
610
|
+
})
|
|
611
|
+
.then(function (result) {
|
|
612
|
+
pending.step += 1;
|
|
613
|
+
if (result && result.pending && typeof result.challenge === "string") {
|
|
614
|
+
// Server-side challenge — `+ <base64>` per RFC 9051 §6.2.2.
|
|
615
|
+
_writeContinuation(socket, result.challenge);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (result && result.ok === true && result.actor) {
|
|
619
|
+
state.actor = result.actor;
|
|
620
|
+
state.stage = "authenticated";
|
|
621
|
+
var savedTag = pending.tag;
|
|
622
|
+
state.authPending = null;
|
|
623
|
+
_emit("mail.server.imap.auth_success",
|
|
624
|
+
{ connectionId: state.id, mechanism: pending.mechanism,
|
|
625
|
+
tenantId: result.actor.tenantId || null });
|
|
626
|
+
_writeTagged(socket, savedTag, "OK [CAPABILITY " + _capabilityLine(state) + "] AUTHENTICATE completed");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
var failTag = pending.tag;
|
|
630
|
+
state.authPending = null;
|
|
631
|
+
rateLimit.noteAuthFailure(state.remoteAddress);
|
|
632
|
+
_emit("mail.server.imap.auth_failed",
|
|
633
|
+
{ connectionId: state.id, mechanism: pending.mechanism,
|
|
634
|
+
reason: (result && result.reason) || "verify-returned-fail" }, "denied");
|
|
635
|
+
_writeTagged(socket, failTag, "NO Authentication credentials invalid");
|
|
636
|
+
})
|
|
637
|
+
.catch(function (err) {
|
|
638
|
+
var failTag = pending.tag;
|
|
639
|
+
state.authPending = null;
|
|
640
|
+
rateLimit.noteAuthFailure(state.remoteAddress);
|
|
641
|
+
_emit("mail.server.imap.auth_failed",
|
|
642
|
+
{ connectionId: state.id, mechanism: pending.mechanism,
|
|
643
|
+
reason: (err && err.message) || String(err) }, "failure");
|
|
644
|
+
_writeTagged(socket, failTag, "NO Authentication failed");
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function _handleLogin(state, socket, tag, args) {
|
|
649
|
+
// RFC 9051 §6.3.4 — LOGIN is deprecated; new MUAs use AUTHENTICATE.
|
|
650
|
+
if (state.actor) {
|
|
651
|
+
_writeTagged(socket, tag, "BAD Already authenticated");
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (profile === "strict") {
|
|
655
|
+
_writeTagged(socket, tag, "BAD LOGIN deprecated under strict profile; use AUTHENTICATE");
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (!state.tls && profile !== "permissive") {
|
|
659
|
+
_writeTagged(socket, tag, "BAD LOGIN requires TLS (use STARTTLS first)");
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (!authConfig || typeof authConfig.verify !== "function") {
|
|
663
|
+
_writeTagged(socket, tag, "NO AUTH not configured");
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
var authAdmit = rateLimit.checkAuthAdmit(state.remoteAddress);
|
|
667
|
+
if (!authAdmit.ok) {
|
|
668
|
+
_writeTagged(socket, tag, "NO [ALERT] Too many AUTH failures from your IP");
|
|
669
|
+
_close(socket, state);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
// LOGIN args: `user pass` (quoted or atom).
|
|
673
|
+
var parts = _parseLoginArgs(args);
|
|
674
|
+
if (!parts) {
|
|
675
|
+
_writeTagged(socket, tag, "BAD LOGIN expects user + pass");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
Promise.resolve()
|
|
679
|
+
.then(function () {
|
|
680
|
+
return authConfig.verify("LOGIN", {
|
|
681
|
+
step: 0,
|
|
682
|
+
username: parts[0],
|
|
683
|
+
password: parts[1],
|
|
684
|
+
tls: state.tls,
|
|
685
|
+
remoteAddress: state.remoteAddress,
|
|
686
|
+
});
|
|
687
|
+
})
|
|
688
|
+
.then(function (result) {
|
|
689
|
+
if (result && result.ok && result.actor) {
|
|
690
|
+
state.actor = result.actor;
|
|
691
|
+
state.stage = "authenticated";
|
|
692
|
+
_emit("mail.server.imap.auth_success",
|
|
693
|
+
{ connectionId: state.id, mechanism: "LOGIN", tenantId: result.actor.tenantId || null });
|
|
694
|
+
_writeTagged(socket, tag, "OK [CAPABILITY " + _capabilityLine(state) + "] LOGIN completed");
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
rateLimit.noteAuthFailure(state.remoteAddress);
|
|
698
|
+
_emit("mail.server.imap.auth_failed",
|
|
699
|
+
{ connectionId: state.id, mechanism: "LOGIN", reason: "verify-returned-fail" }, "denied");
|
|
700
|
+
_writeTagged(socket, tag, "NO LOGIN credentials invalid");
|
|
701
|
+
})
|
|
702
|
+
.catch(function () {
|
|
703
|
+
rateLimit.noteAuthFailure(state.remoteAddress);
|
|
704
|
+
_writeTagged(socket, tag, "NO LOGIN failed");
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function _parseLoginArgs(args) {
|
|
709
|
+
if (typeof args !== "string") return null;
|
|
710
|
+
// Quoted or atom — simple parser sufficient for happy path.
|
|
711
|
+
var rest = args.trim();
|
|
712
|
+
function _take() {
|
|
713
|
+
if (rest[0] === "\"") {
|
|
714
|
+
var end = rest.indexOf("\"", 1);
|
|
715
|
+
if (end === -1) return null;
|
|
716
|
+
var v = rest.slice(1, end);
|
|
717
|
+
rest = rest.slice(end + 1).trim();
|
|
718
|
+
return v;
|
|
719
|
+
}
|
|
720
|
+
var sp = rest.indexOf(" ");
|
|
721
|
+
var v2 = sp === -1 ? rest : rest.slice(0, sp);
|
|
722
|
+
rest = sp === -1 ? "" : rest.slice(sp + 1).trim();
|
|
723
|
+
return v2;
|
|
724
|
+
}
|
|
725
|
+
var user = _take(); if (user === null) return null;
|
|
726
|
+
var pass = _take(); if (pass === null) return null;
|
|
727
|
+
return [user, pass];
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function _requireAuth(state, socket, tag) {
|
|
731
|
+
if (!state.actor) {
|
|
732
|
+
_writeTagged(socket, tag, "NO Login first");
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function _handleSelect(state, socket, tag, args, examine) {
|
|
739
|
+
if (!_requireAuth(state, socket, tag)) return;
|
|
740
|
+
var name = _unquote(args.trim());
|
|
741
|
+
if (!_validateMailboxName(name, { allowLegacyMUtf7: allowLegacyMUtf7 })) {
|
|
742
|
+
_writeTagged(socket, tag, "BAD Mailbox name refused");
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
Promise.resolve()
|
|
746
|
+
.then(function () {
|
|
747
|
+
if (typeof mailStore.selectFolder === "function") {
|
|
748
|
+
return mailStore.selectFolder(state.actor, name, { readOnly: examine });
|
|
749
|
+
}
|
|
750
|
+
// Fallback shape — operators without selectFolder get a minimal
|
|
751
|
+
// OK with sentinel UIDVALIDITY 1.
|
|
752
|
+
return { exists: 0, recent: 0, uidvalidity: 1, uidnext: 1, modseq: 0, flags: [] };
|
|
753
|
+
})
|
|
754
|
+
.then(function (info) {
|
|
755
|
+
state.selectedMailbox = name;
|
|
756
|
+
state.selectedReadOnly = !!examine;
|
|
757
|
+
state.stage = "selected";
|
|
758
|
+
var flagsStr = (info.flags && info.flags.length) ? info.flags.join(" ") : "\\Seen \\Answered \\Flagged \\Deleted \\Draft";
|
|
759
|
+
_writeUntagged(socket, info.exists + " EXISTS");
|
|
760
|
+
_writeUntagged(socket, info.recent + " RECENT");
|
|
761
|
+
_writeUntagged(socket, "FLAGS (" + flagsStr + ")");
|
|
762
|
+
_writeUntagged(socket, "OK [UIDVALIDITY " + info.uidvalidity + "] UIDs valid");
|
|
763
|
+
_writeUntagged(socket, "OK [UIDNEXT " + info.uidnext + "] Predicted next UID");
|
|
764
|
+
if (info.modseq !== undefined) {
|
|
765
|
+
_writeUntagged(socket, "OK [HIGHESTMODSEQ " + info.modseq + "]");
|
|
766
|
+
}
|
|
767
|
+
_emit("mail.server.imap.select", {
|
|
768
|
+
connectionId: state.id, mailbox: name,
|
|
769
|
+
modseq: info.modseq || 0, exists: info.exists,
|
|
770
|
+
});
|
|
771
|
+
_writeTagged(socket, tag, "OK [" + (examine ? "READ-ONLY" : "READ-WRITE") + "] " +
|
|
772
|
+
(examine ? "EXAMINE" : "SELECT") + " completed");
|
|
773
|
+
})
|
|
774
|
+
.catch(function (err) {
|
|
775
|
+
_writeTagged(socket, tag, "NO " + ((err && err.message) || "Select failed").slice(0, ERR_CLAMP));
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function _handleList(state, socket, tag, args) {
|
|
780
|
+
if (!_requireAuth(state, socket, tag)) return;
|
|
781
|
+
// RFC 9051 §6.3.9 — LIST reference mailbox-pattern. Minimal
|
|
782
|
+
// implementation delegates to mailStore.listFolders if present.
|
|
783
|
+
void args;
|
|
784
|
+
Promise.resolve()
|
|
785
|
+
.then(function () {
|
|
786
|
+
if (typeof mailStore.listFolders === "function") {
|
|
787
|
+
return mailStore.listFolders(state.actor);
|
|
788
|
+
}
|
|
789
|
+
return [{ name: "INBOX", attributes: [] }];
|
|
790
|
+
})
|
|
791
|
+
.then(function (folders) {
|
|
792
|
+
for (var i = 0; i < folders.length; i += 1) {
|
|
793
|
+
var f = folders[i];
|
|
794
|
+
var attrs = (f.attributes || []).map(function (a) { return "\\" + a; }).join(" ");
|
|
795
|
+
_writeUntagged(socket, "LIST (" + attrs + ") \"/\" " + _quote(f.name));
|
|
796
|
+
}
|
|
797
|
+
_writeTagged(socket, tag, "OK LIST completed");
|
|
798
|
+
})
|
|
799
|
+
.catch(function (err) {
|
|
800
|
+
_writeTagged(socket, tag, "NO " + ((err && err.message) || "List failed").slice(0, ERR_CLAMP));
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function _handleStatus(state, socket, tag, args) {
|
|
805
|
+
if (!_requireAuth(state, socket, tag)) return;
|
|
806
|
+
var match = args.match(/^(\S+|"[^"]+")\s+\(([^)]+)\)$/); // allow:regex-no-length-cap — args length already capped upstream
|
|
807
|
+
if (!match) {
|
|
808
|
+
_writeTagged(socket, tag, "BAD STATUS expects mailbox + paren-list of items");
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
var name = _unquote(match[1]);
|
|
812
|
+
var items = match[2].split(/\s+/);
|
|
813
|
+
if (!_validateMailboxName(name, { allowLegacyMUtf7: allowLegacyMUtf7 })) {
|
|
814
|
+
_writeTagged(socket, tag, "BAD Mailbox name refused");
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
Promise.resolve()
|
|
818
|
+
.then(function () {
|
|
819
|
+
if (typeof mailStore.statusFolder === "function") {
|
|
820
|
+
return mailStore.statusFolder(state.actor, name, items);
|
|
821
|
+
}
|
|
822
|
+
return { MESSAGES: 0, UIDNEXT: 1, UIDVALIDITY: 1, UNSEEN: 0 };
|
|
823
|
+
})
|
|
824
|
+
.then(function (info) {
|
|
825
|
+
var parts = [];
|
|
826
|
+
for (var k = 0; k < items.length; k += 1) {
|
|
827
|
+
var key = items[k].toUpperCase();
|
|
828
|
+
if (info[key] !== undefined) parts.push(key + " " + info[key]);
|
|
829
|
+
}
|
|
830
|
+
_writeUntagged(socket, "STATUS " + _quote(name) + " (" + parts.join(" ") + ")");
|
|
831
|
+
_writeTagged(socket, tag, "OK STATUS completed");
|
|
832
|
+
})
|
|
833
|
+
.catch(function (err) {
|
|
834
|
+
_writeTagged(socket, tag, "NO " + ((err && err.message) || "Status failed").slice(0, ERR_CLAMP));
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function _handleAppend(state, socket, tag, args, literalBody) {
|
|
839
|
+
if (!_requireAuth(state, socket, tag)) return;
|
|
840
|
+
if (!literalBody) {
|
|
841
|
+
_writeTagged(socket, tag, "BAD APPEND requires a literal {N} message");
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
// RFC 9051 §6.3.12 — APPEND mailbox [(flags)] [date-time] literal
|
|
845
|
+
var match = args.match(/^(\S+|"[^"]+")(?:\s+\(([^)]*)\))?(?:\s+("[^"]+"))?$/); // allow:regex-no-length-cap — args length already capped upstream
|
|
846
|
+
if (!match) {
|
|
847
|
+
_writeTagged(socket, tag, "BAD APPEND syntax");
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
var name = _unquote(match[1]);
|
|
851
|
+
var flags = match[2] ? match[2].split(/\s+/).filter(Boolean) : [];
|
|
852
|
+
if (!_validateMailboxName(name, { allowLegacyMUtf7: allowLegacyMUtf7 })) {
|
|
853
|
+
_writeTagged(socket, tag, "BAD Mailbox name refused");
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
Promise.resolve()
|
|
857
|
+
.then(function () {
|
|
858
|
+
return mailStore.appendMessage(name, literalBody, { actor: state.actor, flags: flags });
|
|
859
|
+
})
|
|
860
|
+
.then(function (info) {
|
|
861
|
+
_emit("mail.server.imap.append",
|
|
862
|
+
{ connectionId: state.id, mailbox: name, size: literalBody.length, flags: flags });
|
|
863
|
+
var token = info && info.uid ? "[APPENDUID " + (info.uidvalidity || 0) + " " + info.uid + "] " : "";
|
|
864
|
+
_writeTagged(socket, tag, "OK " + token + "APPEND completed");
|
|
865
|
+
})
|
|
866
|
+
.catch(function (err) {
|
|
867
|
+
_writeTagged(socket, tag, "NO " + ((err && err.message) || "Append failed").slice(0, ERR_CLAMP));
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function _handleClose(state, socket, tag) {
|
|
872
|
+
state.selectedMailbox = null;
|
|
873
|
+
state.selectedReadOnly = false;
|
|
874
|
+
state.stage = "authenticated";
|
|
875
|
+
_writeTagged(socket, tag, "OK CLOSE completed");
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function _handleExpunge(state, socket, tag) {
|
|
879
|
+
if (!state.selectedMailbox) {
|
|
880
|
+
_writeTagged(socket, tag, "NO No mailbox selected");
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
Promise.resolve()
|
|
884
|
+
.then(function () {
|
|
885
|
+
if (typeof mailStore.expungeFolder === "function") {
|
|
886
|
+
return mailStore.expungeFolder(state.actor, state.selectedMailbox);
|
|
887
|
+
}
|
|
888
|
+
return { expunged: [], modseq: 0 };
|
|
889
|
+
})
|
|
890
|
+
.then(function (info) {
|
|
891
|
+
var ex = info.expunged || [];
|
|
892
|
+
for (var i = 0; i < ex.length; i += 1) {
|
|
893
|
+
_writeUntagged(socket, ex[i] + " EXPUNGE");
|
|
894
|
+
}
|
|
895
|
+
_emit("mail.server.imap.expunge",
|
|
896
|
+
{ connectionId: state.id, mailbox: state.selectedMailbox,
|
|
897
|
+
count: ex.length, modseq: info.modseq || 0 });
|
|
898
|
+
_writeTagged(socket, tag, "OK EXPUNGE completed");
|
|
899
|
+
})
|
|
900
|
+
.catch(function (err) {
|
|
901
|
+
_writeTagged(socket, tag, "NO " + ((err && err.message) || "Expunge failed").slice(0, ERR_CLAMP));
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function _handleFetch(state, socket, tag, args, useUid) {
|
|
906
|
+
if (!state.selectedMailbox) {
|
|
907
|
+
_writeTagged(socket, tag, "NO No mailbox selected");
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (typeof mailStore.fetchRange !== "function") {
|
|
911
|
+
_writeTagged(socket, tag, "BAD FETCH backend not configured");
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
var match = args.match(/^(\S+)\s+(.+)$/); // allow:regex-no-length-cap — args length already capped upstream
|
|
915
|
+
if (!match) {
|
|
916
|
+
_writeTagged(socket, tag, "BAD FETCH expects sequence-set + parts");
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
var seqSet = match[1];
|
|
920
|
+
var partsSpec = match[2];
|
|
921
|
+
Promise.resolve()
|
|
922
|
+
.then(function () {
|
|
923
|
+
// useUid: true tells the backend to interpret seqSet as UIDs
|
|
924
|
+
// per RFC 9051 §6.4.9 — distinct from message-sequence-numbers
|
|
925
|
+
// under the SELECT context. UID FETCH responses MUST include
|
|
926
|
+
// the UID in the parts list per §6.4.9 ("the server SHOULD also
|
|
927
|
+
// include UID information in its response").
|
|
928
|
+
return mailStore.fetchRange(state.actor, state.selectedMailbox, seqSet, partsSpec,
|
|
929
|
+
{ useUid: useUid === true });
|
|
930
|
+
})
|
|
931
|
+
.then(function (rows) {
|
|
932
|
+
var rs = rows || [];
|
|
933
|
+
_emit("mail.server.imap.fetch_bulk",
|
|
934
|
+
{ connectionId: state.id, mailbox: state.selectedMailbox, count: rs.length });
|
|
935
|
+
for (var i = 0; i < rs.length; i += 1) {
|
|
936
|
+
var r = rs[i];
|
|
937
|
+
_writeUntagged(socket, r.seq + " FETCH (" + (r.payload || "") + ")");
|
|
938
|
+
}
|
|
939
|
+
_writeTagged(socket, tag, "OK FETCH completed");
|
|
940
|
+
})
|
|
941
|
+
.catch(function (err) {
|
|
942
|
+
_writeTagged(socket, tag, "NO " + ((err && err.message) || "Fetch failed").slice(0, ERR_CLAMP));
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function _handleStore(state, socket, tag, args, useUid) {
|
|
947
|
+
if (!state.selectedMailbox) {
|
|
948
|
+
_writeTagged(socket, tag, "NO No mailbox selected");
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (state.selectedReadOnly) {
|
|
952
|
+
_writeTagged(socket, tag, "NO Mailbox is read-only");
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (typeof mailStore.storeFlags !== "function") {
|
|
956
|
+
_writeTagged(socket, tag, "BAD STORE backend not configured");
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
var match = args.match(/^(\S+)\s+([+-]?FLAGS(?:\.SILENT)?)\s+\(([^)]*)\)$/i); // allow:regex-no-length-cap — args length already capped upstream
|
|
960
|
+
if (!match) {
|
|
961
|
+
_writeTagged(socket, tag, "BAD STORE expects seq-set FLAGS (...)");
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
var seqSet = match[1];
|
|
965
|
+
var op = match[2].toUpperCase();
|
|
966
|
+
var flagsArr = match[3].split(/\s+/).filter(Boolean);
|
|
967
|
+
var silent = /\.SILENT$/i.test(op);
|
|
968
|
+
var mode = op[0] === "+" ? "add" : op[0] === "-" ? "remove" : "replace";
|
|
969
|
+
Promise.resolve()
|
|
970
|
+
.then(function () {
|
|
971
|
+
return mailStore.storeFlags(state.actor, state.selectedMailbox, seqSet, mode, flagsArr,
|
|
972
|
+
{ useUid: useUid === true });
|
|
973
|
+
})
|
|
974
|
+
.then(function (rows) {
|
|
975
|
+
if (!silent) {
|
|
976
|
+
var rs = rows || [];
|
|
977
|
+
for (var i = 0; i < rs.length; i += 1) {
|
|
978
|
+
var r = rs[i];
|
|
979
|
+
_writeUntagged(socket, r.seq + " FETCH (FLAGS (" + (r.flags || []).join(" ") + "))");
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
_writeTagged(socket, tag, "OK STORE completed");
|
|
983
|
+
})
|
|
984
|
+
.catch(function (err) {
|
|
985
|
+
_writeTagged(socket, tag, "NO " + ((err && err.message) || "Store failed").slice(0, ERR_CLAMP));
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function _handleUid(state, socket, tag, args) {
|
|
990
|
+
// UID FETCH / UID STORE / UID SEARCH / UID COPY / UID MOVE per
|
|
991
|
+
// RFC 9051 §6.4.9. The sub-command's sequence-set is interpreted
|
|
992
|
+
// as UIDs (not message-sequence-numbers); we pass `useUid: true`
|
|
993
|
+
// to the sub-handler which threads it through to the backend's
|
|
994
|
+
// mailStore.fetchRange / storeFlags via opts. Without this, the
|
|
995
|
+
// backend treats the seq-set as msg-numbers and a client's
|
|
996
|
+
// `UID FETCH 12345 (BODY[])` returns the WRONG message.
|
|
997
|
+
var sub = args.match(/^(\S+)\s+(.+)$/); // allow:regex-no-length-cap — args length already capped upstream
|
|
998
|
+
if (!sub) {
|
|
999
|
+
_writeTagged(socket, tag, "BAD UID expects a sub-command");
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
var subVerb = sub[1].toUpperCase();
|
|
1003
|
+
var subArgs = sub[2];
|
|
1004
|
+
if (subVerb === "FETCH") return _handleFetch(state, socket, tag, subArgs, true);
|
|
1005
|
+
if (subVerb === "STORE") return _handleStore(state, socket, tag, subArgs, true);
|
|
1006
|
+
_writeTagged(socket, tag, "BAD UID " + subVerb + " not implemented in v1");
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function _handleIdle(state, socket, tag) {
|
|
1010
|
+
if (!_requireAuth(state, socket, tag)) return;
|
|
1011
|
+
_writeContinuation(socket, "idling");
|
|
1012
|
+
// RFC 2177 §3 — IDLE must be terminated with DONE before
|
|
1013
|
+
// bandwidth-timeout. We schedule a soft cutoff 1 min before the
|
|
1014
|
+
// hard 30-min cutoff to force client re-issue.
|
|
1015
|
+
var timer = setTimeout(function () {
|
|
1016
|
+
if (state.idle) {
|
|
1017
|
+
_writeUntagged(socket, "BYE IDLE timed out — re-issue");
|
|
1018
|
+
state.idle = null;
|
|
1019
|
+
_close(socket, state);
|
|
1020
|
+
}
|
|
1021
|
+
}, IDLE_BANDWIDTH_TIMEOUT_MS);
|
|
1022
|
+
state.idle = { tag: tag, timer: timer };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function _writeTagged(socket, tag, msg) {
|
|
1026
|
+
try { socket.write(tag + " " + msg + "\r\n"); }
|
|
1027
|
+
catch (_e) { /* socket may be down */ }
|
|
1028
|
+
}
|
|
1029
|
+
function _writeUntagged(socket, msg) {
|
|
1030
|
+
try { socket.write("* " + msg + "\r\n"); }
|
|
1031
|
+
catch (_e) { /* socket may be down */ }
|
|
1032
|
+
}
|
|
1033
|
+
function _writeContinuation(socket, msg) {
|
|
1034
|
+
try { socket.write("+ " + msg + "\r\n"); }
|
|
1035
|
+
catch (_e) { /* socket may be down */ }
|
|
1036
|
+
}
|
|
1037
|
+
function _close(socket, state) {
|
|
1038
|
+
// The drain loop's `if (state.stage === "closed") return;` guard
|
|
1039
|
+
// (around the bottom of _drainBuffer) was dead before this —
|
|
1040
|
+
// _close never wrote the sentinel, so the drain loop kept
|
|
1041
|
+
// processing buffered bytes after the socket was destroyed.
|
|
1042
|
+
// Setting stage="closed" here makes the guard reachable so a
|
|
1043
|
+
// close mid-loop short-circuits the next command dispatch
|
|
1044
|
+
// (defense-in-depth against an exception thrown by a handler
|
|
1045
|
+
// that doesn't tear down the loop).
|
|
1046
|
+
if (state && typeof state === "object") state.stage = "closed";
|
|
1047
|
+
try { socket.end(); } catch (_e) { /* idempotent */ }
|
|
1048
|
+
try { socket.destroy(); } catch (_e2) { /* idempotent */ }
|
|
1049
|
+
connections.delete(socket);
|
|
1050
|
+
}
|
|
1051
|
+
function _quote(s) { return '"' + String(s).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"'; }
|
|
1052
|
+
function _unquote(s) {
|
|
1053
|
+
if (typeof s !== "string") return "";
|
|
1054
|
+
if (s[0] === "\"" && s[s.length - 1] === "\"") return s.slice(1, -1);
|
|
1055
|
+
return s;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// ---- Lifecycle ----------------------------------------------------------
|
|
1059
|
+
async function listen(listenOpts) {
|
|
1060
|
+
listenOpts = listenOpts || {};
|
|
1061
|
+
if (listening) {
|
|
1062
|
+
throw new MailServerImapError("mail-server-imap/already-listening",
|
|
1063
|
+
"listen: already listening");
|
|
1064
|
+
}
|
|
1065
|
+
var port = listenOpts.port === undefined ? 143 : listenOpts.port; // allow:raw-byte-literal — RFC 9051 IMAP port (IANA)
|
|
1066
|
+
var address = listenOpts.address || "0.0.0.0";
|
|
1067
|
+
tcpServer = net.createServer(function (socket) { _handleConnection(socket); });
|
|
1068
|
+
return new Promise(function (resolve, reject) {
|
|
1069
|
+
tcpServer.once("error", reject);
|
|
1070
|
+
tcpServer.listen(port, address, function () {
|
|
1071
|
+
listening = true;
|
|
1072
|
+
tcpServer.removeListener("error", reject);
|
|
1073
|
+
_emit("mail.server.imap.listening",
|
|
1074
|
+
{ port: port, address: address });
|
|
1075
|
+
resolve({ port: tcpServer.address().port, address: address });
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function close() {
|
|
1081
|
+
if (!listening) return;
|
|
1082
|
+
listening = false;
|
|
1083
|
+
for (var s of connections) { try { s.destroy(); } catch (_e) { /* idempotent */ } }
|
|
1084
|
+
connections.clear();
|
|
1085
|
+
return new Promise(function (resolve) {
|
|
1086
|
+
tcpServer.close(function () {
|
|
1087
|
+
_emit("mail.server.imap.closed", {});
|
|
1088
|
+
resolve();
|
|
1089
|
+
});
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return {
|
|
1094
|
+
listen: listen,
|
|
1095
|
+
close: close,
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
module.exports = {
|
|
1100
|
+
create: create,
|
|
1101
|
+
MailServerImapError: MailServerImapError,
|
|
1102
|
+
};
|