imapflow 2.4.3 → 2.5.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 +15 -0
- package/README.md +59 -1
- package/dist/cjs/commands/authenticate.js +32 -1
- package/dist/cjs/commands/fetch.js +10 -3
- package/dist/cjs/commands/idle.js +31 -1
- package/dist/cjs/commands/list.js +14 -0
- package/dist/cjs/errors.d.ts +2 -0
- package/dist/cjs/errors.js +4 -0
- package/dist/cjs/handler/imap-parser.js +3 -0
- package/dist/cjs/handler/parser-instance.d.ts +2 -0
- package/dist/cjs/handler/parser-instance.js +3 -1
- package/dist/cjs/handler/token-parser.d.ts +21 -2
- package/dist/cjs/handler/token-parser.js +76 -6
- package/dist/cjs/handler/types.d.ts +5 -0
- package/dist/cjs/imap-flow.d.ts +9 -9
- package/dist/cjs/imap-flow.js +90 -14
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/sockets-transport.d.ts +44 -0
- package/dist/cjs/sockets-transport.js +89 -0
- package/dist/cjs/streams-transport.d.ts +84 -0
- package/dist/cjs/streams-transport.js +281 -0
- package/dist/cjs/tools.d.ts +30 -0
- package/dist/cjs/tools.js +130 -52
- package/dist/cjs/types.d.ts +78 -2
- package/dist/esm/commands/authenticate.js +32 -1
- package/dist/esm/commands/fetch.js +11 -4
- package/dist/esm/commands/idle.js +31 -1
- package/dist/esm/commands/list.js +15 -1
- package/dist/esm/errors.d.ts +2 -0
- package/dist/esm/errors.js +4 -0
- package/dist/esm/handler/imap-parser.js +3 -0
- package/dist/esm/handler/parser-instance.d.ts +2 -0
- package/dist/esm/handler/parser-instance.js +3 -1
- package/dist/esm/handler/token-parser.d.ts +21 -2
- package/dist/esm/handler/token-parser.js +76 -6
- package/dist/esm/handler/types.d.ts +5 -0
- package/dist/esm/imap-flow.d.ts +9 -9
- package/dist/esm/imap-flow.js +88 -14
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/sockets-transport.d.ts +44 -0
- package/dist/esm/sockets-transport.js +86 -0
- package/dist/esm/streams-transport.d.ts +84 -0
- package/dist/esm/streams-transport.js +278 -0
- package/dist/esm/tools.d.ts +30 -0
- package/dist/esm/tools.js +126 -51
- package/dist/esm/types.d.ts +78 -2
- package/package.json +16 -5
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The `transport` option over a socket API built on web streams: the socket opens with a promise
|
|
3
|
+
// that resolves to a ReadableStream and a WritableStream of bytes. `cloudflare:sockets` (celld,
|
|
4
|
+
// Cloudflare Workers) is one, see sockets-transport.ts; Chrome's Direct Sockets and in-memory
|
|
5
|
+
// connections fit the same shape. The streams are wrapped into a Duplex that behaves like the
|
|
6
|
+
// net.Socket ImapFlow drives.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.createStreamsTransport = createStreamsTransport;
|
|
9
|
+
const node_stream_1 = require("node:stream");
|
|
10
|
+
const tools_js_1 = require("./tools.js");
|
|
11
|
+
function closeQuietly(socket) {
|
|
12
|
+
try {
|
|
13
|
+
Promise.resolve(socket.close()).catch(tools_js_1.noop);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// already closed
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
class StreamsDuplex extends node_stream_1.Duplex {
|
|
20
|
+
constructor(api, socket, onConnect, plain) {
|
|
21
|
+
super({ allowHalfOpen: false });
|
|
22
|
+
// Taken once the socket is open: locking the streams of a socket returned by startTls()
|
|
23
|
+
// before its handshake completes stalls the handshake on celld
|
|
24
|
+
this._reader = null;
|
|
25
|
+
this._writer = null;
|
|
26
|
+
this._pumping = false;
|
|
27
|
+
this._readRequested = false;
|
|
28
|
+
// A runtime may only start TLS on a socket without a read in flight (with one, celld never
|
|
29
|
+
// completes the handshake), while the read loop always keeps one. So once the client
|
|
30
|
+
// announced a STARTTLS (holdAfterStarttls), reading stops after the tagged line that answers
|
|
31
|
+
// it, the only command in flight then, and resumes only when that line is not an OK.
|
|
32
|
+
this._awaitingStarttls = false;
|
|
33
|
+
this._lineTail = '';
|
|
34
|
+
// Set when the partial line got too long to be the one looked for and was dropped: the rest
|
|
35
|
+
// of it, up to the next line break, is skipped too
|
|
36
|
+
this._tailDropped = false;
|
|
37
|
+
this._readsHeld = false;
|
|
38
|
+
// Set once startTls() handed the connection to a TLS socket: the read that ends then is not
|
|
39
|
+
// the end of the connection, and closing this socket would close the TLS one
|
|
40
|
+
this._upgraded = false;
|
|
41
|
+
this._timeout = 0;
|
|
42
|
+
this._timer = null;
|
|
43
|
+
// Traffic only records the time, the one timer re-arms itself for the rest of the idle period
|
|
44
|
+
this._lastActivity = 0;
|
|
45
|
+
this._api = api;
|
|
46
|
+
this._socket = socket;
|
|
47
|
+
this._plain = plain || null;
|
|
48
|
+
socket.opened.then(info => {
|
|
49
|
+
if (this.destroyed) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
this._reader = info.readable.getReader();
|
|
54
|
+
this._writer = info.writable.getWriter();
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
// streams already locked by someone else
|
|
58
|
+
this.destroy(err);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.remoteAddress = info.remoteAddress;
|
|
62
|
+
this.remotePort = info.remotePort;
|
|
63
|
+
this.localAddress = info.localAddress;
|
|
64
|
+
this.localPort = info.localPort;
|
|
65
|
+
const tls = info.tls;
|
|
66
|
+
if (tls) {
|
|
67
|
+
this.authorized = tls.authorized;
|
|
68
|
+
this.getCipher = () => ({ name: tls.name || '', standardName: tls.standardName || tls.name || '', version: tls.version || '' });
|
|
69
|
+
}
|
|
70
|
+
this._touch();
|
|
71
|
+
onConnect();
|
|
72
|
+
if (this._readRequested) {
|
|
73
|
+
void this._pump();
|
|
74
|
+
}
|
|
75
|
+
}, (err) => this.destroy(err));
|
|
76
|
+
socket.closed?.catch((err) => {
|
|
77
|
+
if (!this._upgraded) {
|
|
78
|
+
this.destroy(err);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
_read() {
|
|
83
|
+
if (this._readsHeld) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!this._reader) {
|
|
87
|
+
this._readRequested = true;
|
|
88
|
+
}
|
|
89
|
+
else if (!this._pumping) {
|
|
90
|
+
void this._pump();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async _pump() {
|
|
94
|
+
const reader = this._reader;
|
|
95
|
+
this._pumping = true;
|
|
96
|
+
try {
|
|
97
|
+
for (;;) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (this._upgraded || this.destroyed) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (done || !value) {
|
|
103
|
+
this.push(null);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this._touch();
|
|
107
|
+
const chunk = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
108
|
+
const pushed = this.push(chunk);
|
|
109
|
+
if (this._awaitingStarttls && this._starttlsCompleted(chunk)) {
|
|
110
|
+
this._readsHeld = true;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!pushed) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (!this._upgraded) {
|
|
120
|
+
this.destroy(err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
this._pumping = false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Whether the chunk holds the tagged response of the STARTTLS command and it is an OK. Lines
|
|
128
|
+
// end with LF, CR is optional, as for the parser
|
|
129
|
+
_starttlsCompleted(chunk) {
|
|
130
|
+
const skipFirst = this._tailDropped;
|
|
131
|
+
const lines = (this._lineTail + chunk.toString('latin1')).split('\n');
|
|
132
|
+
const tail = lines.pop();
|
|
133
|
+
if (skipFirst && !lines.length) {
|
|
134
|
+
// still inside the dropped line
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
this._tailDropped = tail.length > 1024;
|
|
138
|
+
this._lineTail = this._tailDropped ? '' : tail;
|
|
139
|
+
for (let i = skipFirst ? 1 : 0; i < lines.length; i++) {
|
|
140
|
+
const tagged = /^(?![*+] )\S+ (OK|NO|BAD)\b/i.exec(lines[i]);
|
|
141
|
+
if (tagged) {
|
|
142
|
+
this._awaitingStarttls = false;
|
|
143
|
+
return tagged[1].toUpperCase() === 'OK';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
/** Stops reading after the tagged response of the STARTTLS command about to be sent, if it is an OK */
|
|
149
|
+
holdAfterStarttls() {
|
|
150
|
+
this._awaitingStarttls = true;
|
|
151
|
+
this._lineTail = '';
|
|
152
|
+
this._tailDropped = false;
|
|
153
|
+
}
|
|
154
|
+
_write(chunk, _encoding, callback) {
|
|
155
|
+
if (!this._writer) {
|
|
156
|
+
// ImapFlow writes only after onConnect, this is a misuse
|
|
157
|
+
callback(new Error('Socket is not open yet'));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this._touch();
|
|
161
|
+
this._writer.write(chunk).then(() => callback(), callback);
|
|
162
|
+
}
|
|
163
|
+
_destroy(err, callback) {
|
|
164
|
+
this._clearTimer();
|
|
165
|
+
if (!this._upgraded) {
|
|
166
|
+
this._close();
|
|
167
|
+
}
|
|
168
|
+
if (this._plain) {
|
|
169
|
+
// the plain connection went with the TLS one, unless the API keeps it open
|
|
170
|
+
this._plain._close();
|
|
171
|
+
this._plain.destroy();
|
|
172
|
+
}
|
|
173
|
+
callback(err);
|
|
174
|
+
}
|
|
175
|
+
// Closes the connection. An API that refuses to close while its streams are locked (Direct
|
|
176
|
+
// Sockets) gets a second attempt with the locks released; releasing them right away would
|
|
177
|
+
// fail the read in flight, which ends the request on celld
|
|
178
|
+
_close() {
|
|
179
|
+
const retry = () => {
|
|
180
|
+
for (const stream of [this._reader, this._writer]) {
|
|
181
|
+
try {
|
|
182
|
+
stream?.releaseLock();
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// an older streams implementation refuses with a read in flight
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
closeQuietly(this._socket);
|
|
189
|
+
};
|
|
190
|
+
try {
|
|
191
|
+
const closing = this._socket.close();
|
|
192
|
+
if (typeof closing?.then === 'function') {
|
|
193
|
+
closing.then(undefined, retry);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
retry();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** Like net.Socket#setTimeout(): emits 'timeout' after `ms` without traffic, 0 disables */
|
|
201
|
+
setTimeout(ms) {
|
|
202
|
+
this._timeout = ms;
|
|
203
|
+
this._clearTimer();
|
|
204
|
+
this._touch();
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
setKeepAlive() {
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
/** Hands the connection over to a TLS socket, this one only stays as its transport */
|
|
211
|
+
startTls(options, onSecure) {
|
|
212
|
+
this._upgraded = true;
|
|
213
|
+
this._clearTimer();
|
|
214
|
+
// workerd refuses to start TLS while the streams are locked, nothing is in flight on them
|
|
215
|
+
this._reader?.releaseLock();
|
|
216
|
+
this._writer?.releaseLock();
|
|
217
|
+
let tlsSocket;
|
|
218
|
+
try {
|
|
219
|
+
tlsSocket = this._api.upgrade(this._socket, options);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
// refused before any handshake, the connection is closed with this socket
|
|
223
|
+
this._upgraded = false;
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
return new StreamsDuplex(this._api, tlsSocket, onSecure, this);
|
|
227
|
+
}
|
|
228
|
+
_touch() {
|
|
229
|
+
this._lastActivity = Date.now();
|
|
230
|
+
if (!this._timer) {
|
|
231
|
+
this._arm(this._timeout);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
_arm(ms) {
|
|
235
|
+
if (this._timeout > 0 && !this.destroyed && !this._upgraded) {
|
|
236
|
+
this._timer = (0, tools_js_1.unrefTimer)(setTimeout(() => {
|
|
237
|
+
this._timer = null;
|
|
238
|
+
const idle = Date.now() - this._lastActivity;
|
|
239
|
+
if (idle >= this._timeout) {
|
|
240
|
+
this.emit('timeout');
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
this._arm(this._timeout - idle);
|
|
244
|
+
}
|
|
245
|
+
}, ms));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
_clearTimer() {
|
|
249
|
+
(0, tools_js_1.clearTimer)(this._timer);
|
|
250
|
+
this._timer = null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Builds a transport for the `transport` option over a socket API built on web streams. A new
|
|
255
|
+
* API needs only `open()`, and `upgrade()` if it can start TLS on an open connection:
|
|
256
|
+
*
|
|
257
|
+
* ```js
|
|
258
|
+
* const transport = createStreamsTransport({
|
|
259
|
+
* open: ({ host, port, secure }) => {
|
|
260
|
+
* const socket = openSomeSocket(host, port, secure);
|
|
261
|
+
* return { opened: socket.ready.then(() => ({ readable: socket.readable, writable: socket.writable })), close: () => socket.close() };
|
|
262
|
+
* }
|
|
263
|
+
* });
|
|
264
|
+
* ```
|
|
265
|
+
*
|
|
266
|
+
* The transport stops reading once the server accepted a STARTTLS, so the API never has to start
|
|
267
|
+
* TLS with a read in flight, and releases the stream locks before `upgrade()` is called.
|
|
268
|
+
*
|
|
269
|
+
* @param api - Opens (and upgrades) connections
|
|
270
|
+
* @returns A transport for the `transport` option
|
|
271
|
+
*/
|
|
272
|
+
function createStreamsTransport(api) {
|
|
273
|
+
const transport = {
|
|
274
|
+
connect: (options, onConnect) => new StreamsDuplex(api, api.open(options), onConnect)
|
|
275
|
+
};
|
|
276
|
+
if (api.upgrade) {
|
|
277
|
+
transport.prepareUpgrade = socket => socket.holdAfterStarttls();
|
|
278
|
+
transport.upgrade = (socket, options, onSecure) => socket.startTls(options, onSecure);
|
|
279
|
+
}
|
|
280
|
+
return transport;
|
|
281
|
+
}
|
package/dist/cjs/tools.d.ts
CHANGED
|
@@ -5,8 +5,18 @@ import type { ImapAttributeList, ImapAttributeNode, ImapResponse } from './handl
|
|
|
5
5
|
import type { FetchMessageObject, ListResponse, ListTreeResponse, MailboxObject, MessageEnvelopeObject, MessageStructureObject, ImapFlowEvents, StatusQuery } from './types.js';
|
|
6
6
|
export { AuthenticationFailure } from './errors.js';
|
|
7
7
|
export declare const EXPANDED_RANGE_LIMIT = 16777216;
|
|
8
|
+
export declare const MAX_COLLECTED_RESPONSES = 1000000;
|
|
8
9
|
export declare const MAX_UINT32_DIGITS = 10;
|
|
9
10
|
export declare const noop: () => void;
|
|
11
|
+
/**
|
|
12
|
+
* Tells an IP literal from a hostname without `node:net`, which some runtimes do not implement.
|
|
13
|
+
* Only used to decide whether a host can be a TLS servername: a dotted quad, or anything with a
|
|
14
|
+
* colon (an IPv6 literal, a hostname never contains one).
|
|
15
|
+
*
|
|
16
|
+
* @param host - Hostname or IP address
|
|
17
|
+
* @returns true for an IP literal
|
|
18
|
+
*/
|
|
19
|
+
export declare function isIpLiteral(host: string): boolean;
|
|
10
20
|
/**
|
|
11
21
|
* A stream decoder returned by getDecoder(). The Japanese decoder reports the `limited`
|
|
12
22
|
* flag once it has buffered all it will accept; a streaming iconv decoder never sets it.
|
|
@@ -462,6 +472,26 @@ export declare function parseBigIntValue(value: unknown, maxDigits?: number): bi
|
|
|
462
472
|
* @returns The parsed value, or false when it is not usable.
|
|
463
473
|
*/
|
|
464
474
|
export declare function parseUintValue(value: unknown, maxDigits?: number): number | false;
|
|
475
|
+
/**
|
|
476
|
+
* Walks an IMAP sequence range string (e.g. "1:3,5,7:9") one number at a time, without
|
|
477
|
+
* materializing it. A single "1:16777216" is a few bytes on the wire but sixteen million
|
|
478
|
+
* entries expanded, so a consumer that only needs to visit each number (the VANISHED handler)
|
|
479
|
+
* iterates instead of calling expandRange().
|
|
480
|
+
*
|
|
481
|
+
* The same bound as expandRange() applies: at most EXPANDED_RANGE_LIMIT numbers in total are
|
|
482
|
+
* produced, over all entries of the set.
|
|
483
|
+
*
|
|
484
|
+
* @param range - IMAP sequence range string
|
|
485
|
+
* @returns The sequence numbers, in the order the set lists them
|
|
486
|
+
*/
|
|
487
|
+
export declare function iterateRange(range: unknown): Generator<number, void, undefined>;
|
|
488
|
+
/**
|
|
489
|
+
* Counts the numbers iterateRange() produces for a sequence range string, without walking it.
|
|
490
|
+
*
|
|
491
|
+
* @param range - IMAP sequence range string
|
|
492
|
+
* @returns The number of entries, capped at EXPANDED_RANGE_LIMIT like the expansion itself
|
|
493
|
+
*/
|
|
494
|
+
export declare function countRange(range: unknown): number;
|
|
465
495
|
/**
|
|
466
496
|
* Expands an IMAP sequence range string (e.g. "1:3,5,7:9") into an array of numbers.
|
|
467
497
|
*
|
package/dist/cjs/tools.js
CHANGED
|
@@ -4,7 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.noop = exports.MAX_UINT32_DIGITS = exports.EXPANDED_RANGE_LIMIT = exports.AuthenticationFailure = void 0;
|
|
7
|
+
exports.noop = exports.MAX_UINT32_DIGITS = exports.MAX_COLLECTED_RESPONSES = exports.EXPANDED_RANGE_LIMIT = exports.AuthenticationFailure = void 0;
|
|
8
|
+
exports.isIpLiteral = isIpLiteral;
|
|
8
9
|
exports.buildConnectionError = buildConnectionError;
|
|
9
10
|
exports.restampConnectionError = restampConnectionError;
|
|
10
11
|
exports.guardedPromise = guardedPromise;
|
|
@@ -52,6 +53,8 @@ exports.getStringList = getStringList;
|
|
|
52
53
|
exports.getStringValue = getStringValue;
|
|
53
54
|
exports.parseBigIntValue = parseBigIntValue;
|
|
54
55
|
exports.parseUintValue = parseUintValue;
|
|
56
|
+
exports.iterateRange = iterateRange;
|
|
57
|
+
exports.countRange = countRange;
|
|
55
58
|
exports.expandRange = expandRange;
|
|
56
59
|
exports.getDecoder = getDecoder;
|
|
57
60
|
exports.packMessageRange = packMessageRange;
|
|
@@ -76,6 +79,15 @@ const CONNECTION_GONE_CODES = new Set([errors_js_1.ImapFlowErrorCode.NoConnectio
|
|
|
76
79
|
// expand server sequences (e.g. ESEARCH ALL in commands/search.ts) can apply the same absolute
|
|
77
80
|
// ceiling instead of inventing their own.
|
|
78
81
|
exports.EXPANDED_RANGE_LIMIT = 0x1000000;
|
|
82
|
+
// Upper bound for the untagged responses one command may accumulate in memory: the FETCH rows
|
|
83
|
+
// fetchAll() collects and the mailboxes list() collects. The response size limits bound one
|
|
84
|
+
// response at a time, not how many of them a server sends in reply to one command, so a hostile
|
|
85
|
+
// server answering a single-message FETCH with an endless stream of unsolicited FETCH rows (or a
|
|
86
|
+
// LIST with endless mailboxes) would otherwise grow the process without limit. Generous on
|
|
87
|
+
// purpose: a million rows is far beyond any legitimate listing, and a mailbox that large is read
|
|
88
|
+
// through the streaming fetch() generator, which keeps nothing. Reaching it therefore fails the
|
|
89
|
+
// command (code TooManyResponses) instead of silently truncating its result.
|
|
90
|
+
exports.MAX_COLLECTED_RESPONSES = 1000000;
|
|
79
91
|
// Digit bounds for untrusted numeric values in server responses. UIDs, UIDVALIDITY and
|
|
80
92
|
// message counts are 32-bit unsigned (nz-number in the RFC 9051 grammar, so at most 10
|
|
81
93
|
// digits); MODSEQ and number64 values are 63-bit unsigned (RFC 7162, RFC 9051), at most
|
|
@@ -122,6 +134,17 @@ const IMAP4REV2_FOLDED_CAPABILITIES = new Set([
|
|
|
122
134
|
// Deliberate no-op, used as the observer a guarded promise attaches to its own rejection.
|
|
123
135
|
const noop = () => { };
|
|
124
136
|
exports.noop = noop;
|
|
137
|
+
/**
|
|
138
|
+
* Tells an IP literal from a hostname without `node:net`, which some runtimes do not implement.
|
|
139
|
+
* Only used to decide whether a host can be a TLS servername: a dotted quad, or anything with a
|
|
140
|
+
* colon (an IPv6 literal, a hostname never contains one).
|
|
141
|
+
*
|
|
142
|
+
* @param host - Hostname or IP address
|
|
143
|
+
* @returns true for an IP literal
|
|
144
|
+
*/
|
|
145
|
+
function isIpLiteral(host) {
|
|
146
|
+
return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(':');
|
|
147
|
+
}
|
|
125
148
|
// The fields buildConnectionError() stamps to say *where* a connection error was rejected, as
|
|
126
149
|
// opposed to what went wrong. restampConnectionError() clears them, because they belong to the
|
|
127
150
|
// site that built the error rather than to the failure it describes.
|
|
@@ -594,13 +617,30 @@ function getFolderTree(folders) {
|
|
|
594
617
|
root: true,
|
|
595
618
|
folders: []
|
|
596
619
|
};
|
|
620
|
+
// Children of every node indexed by name. Looking a child up with folders.find() made the
|
|
621
|
+
// tree quadratic in the number of siblings: a listing of 50k mailboxes at one level (a size
|
|
622
|
+
// the server chooses) took seconds of event loop time. A name is unique among its siblings,
|
|
623
|
+
// as the loop below updates an existing entry rather than adding a second one.
|
|
624
|
+
let childIndex = new Map();
|
|
625
|
+
let childByName = (node, name) => {
|
|
626
|
+
let index = childIndex.get(node);
|
|
627
|
+
return index ? index.get(name) : undefined;
|
|
628
|
+
};
|
|
629
|
+
let indexChild = (node, child) => {
|
|
630
|
+
let index = childIndex.get(node);
|
|
631
|
+
if (!index) {
|
|
632
|
+
index = new Map();
|
|
633
|
+
childIndex.set(node, index);
|
|
634
|
+
}
|
|
635
|
+
index.set(child.name, child);
|
|
636
|
+
};
|
|
597
637
|
let getTreeNode = (parents) => {
|
|
598
638
|
let node = tree;
|
|
599
639
|
if (!parents || !parents.length) {
|
|
600
640
|
return node;
|
|
601
641
|
}
|
|
602
642
|
for (let parent of parents) {
|
|
603
|
-
let cur = node
|
|
643
|
+
let cur = childByName(node, parent);
|
|
604
644
|
if (cur) {
|
|
605
645
|
node = cur;
|
|
606
646
|
}
|
|
@@ -615,7 +655,7 @@ function getFolderTree(folders) {
|
|
|
615
655
|
for (let folder of byDepth) {
|
|
616
656
|
let parent = getTreeNode(folder.parent);
|
|
617
657
|
// see if entry already exists
|
|
618
|
-
let existing = parent
|
|
658
|
+
let existing = childByName(parent, folder.name);
|
|
619
659
|
if (existing) {
|
|
620
660
|
// update values
|
|
621
661
|
existing.name = folder.name;
|
|
@@ -660,6 +700,7 @@ function getFolderTree(folders) {
|
|
|
660
700
|
parent.folders = [];
|
|
661
701
|
}
|
|
662
702
|
parent.folders.push(data);
|
|
703
|
+
indexChild(parent, data);
|
|
663
704
|
}
|
|
664
705
|
}
|
|
665
706
|
return tree;
|
|
@@ -1554,67 +1595,103 @@ function parseUintValue(value, maxDigits) {
|
|
|
1554
1595
|
return Number.isSafeInteger(num) ? num : false;
|
|
1555
1596
|
}
|
|
1556
1597
|
/**
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1559
|
-
* Entries with
|
|
1560
|
-
*
|
|
1561
|
-
*
|
|
1562
|
-
* EXPANDED_RANGE_LIMIT entries in total: legitimate responses never reach the limit
|
|
1563
|
-
* (the mailbox would need that many messages), while hostile input is cut off
|
|
1564
|
-
* instead of exhausting memory. The total is capped, not just each range -
|
|
1565
|
-
* otherwise "1:16777216,1:16777216,..." would multiply the per-range bound by an
|
|
1566
|
-
* unbounded number of ranges.
|
|
1598
|
+
* Parses an IMAP sequence set string (e.g. "1:3,5,9:7") into its entries, each as the pair of
|
|
1599
|
+
* endpoints in the order written: a reversed "9:7" stays reversed, and iterateRange() walks it
|
|
1600
|
+
* downwards. Entries with an endpoint that is not a valid nz-number are skipped - the input may
|
|
1601
|
+
* come from an untrusted server, and 'Infinity' or similar garbage would otherwise loop without
|
|
1602
|
+
* bound.
|
|
1567
1603
|
*
|
|
1568
|
-
* @param range - IMAP sequence
|
|
1569
|
-
* @returns
|
|
1604
|
+
* @param range - IMAP sequence set string, or whatever the response parser produced for it
|
|
1605
|
+
* @returns The validated entries
|
|
1570
1606
|
*/
|
|
1571
|
-
function
|
|
1572
|
-
let
|
|
1607
|
+
function parseRangeEntries(range) {
|
|
1608
|
+
let entries = [];
|
|
1573
1609
|
// Callers pass whatever the response parser produced for the sequence set, and a
|
|
1574
1610
|
// malformed response can leave that as `false` (e.g. a VANISHED response carrying
|
|
1575
1611
|
// only the (EARLIER) tag). Nothing to expand then, and throwing here would abort
|
|
1576
1612
|
// the handler for the rest of the response.
|
|
1577
1613
|
if (typeof range !== 'string') {
|
|
1578
|
-
return
|
|
1614
|
+
return entries;
|
|
1579
1615
|
}
|
|
1580
1616
|
for (let entry of range.split(',')) {
|
|
1581
|
-
if (result.length >= exports.EXPANDED_RANGE_LIMIT) {
|
|
1582
|
-
break;
|
|
1583
|
-
}
|
|
1584
1617
|
entry = entry.trim();
|
|
1585
1618
|
let colon = entry.indexOf(':');
|
|
1586
1619
|
if (colon < 0) {
|
|
1587
1620
|
let value = Number(entry);
|
|
1588
1621
|
if (isValidSequenceValue(value)) {
|
|
1589
|
-
|
|
1622
|
+
entries.push([value, value]);
|
|
1590
1623
|
}
|
|
1591
1624
|
continue;
|
|
1592
1625
|
}
|
|
1593
1626
|
let first = Number(entry.substring(0, colon));
|
|
1594
1627
|
let second = Number(entry.slice(colon + 1));
|
|
1595
|
-
if (
|
|
1596
|
-
|
|
1597
|
-
}
|
|
1598
|
-
if (first === second) {
|
|
1599
|
-
result.push(first);
|
|
1600
|
-
continue;
|
|
1628
|
+
if (isValidSequenceValue(first) && isValidSequenceValue(second)) {
|
|
1629
|
+
entries.push([first, second]);
|
|
1601
1630
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1631
|
+
}
|
|
1632
|
+
return entries;
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Walks an IMAP sequence range string (e.g. "1:3,5,7:9") one number at a time, without
|
|
1636
|
+
* materializing it. A single "1:16777216" is a few bytes on the wire but sixteen million
|
|
1637
|
+
* entries expanded, so a consumer that only needs to visit each number (the VANISHED handler)
|
|
1638
|
+
* iterates instead of calling expandRange().
|
|
1639
|
+
*
|
|
1640
|
+
* The same bound as expandRange() applies: at most EXPANDED_RANGE_LIMIT numbers in total are
|
|
1641
|
+
* produced, over all entries of the set.
|
|
1642
|
+
*
|
|
1643
|
+
* @param range - IMAP sequence range string
|
|
1644
|
+
* @returns The sequence numbers, in the order the set lists them
|
|
1645
|
+
*/
|
|
1646
|
+
function* iterateRange(range) {
|
|
1647
|
+
let count = 0;
|
|
1648
|
+
for (let [first, second] of parseRangeEntries(range)) {
|
|
1649
|
+
let step = first <= second ? 1 : -1;
|
|
1650
|
+
for (let i = first; count < exports.EXPANDED_RANGE_LIMIT; i += step) {
|
|
1651
|
+
yield i;
|
|
1652
|
+
count++;
|
|
1653
|
+
if (i === second) {
|
|
1654
|
+
break;
|
|
1608
1655
|
}
|
|
1609
1656
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
for (let i = first; i >= last; i--) {
|
|
1613
|
-
result.push(i);
|
|
1614
|
-
}
|
|
1657
|
+
if (count >= exports.EXPANDED_RANGE_LIMIT) {
|
|
1658
|
+
break;
|
|
1615
1659
|
}
|
|
1616
1660
|
}
|
|
1617
|
-
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Counts the numbers iterateRange() produces for a sequence range string, without walking it.
|
|
1664
|
+
*
|
|
1665
|
+
* @param range - IMAP sequence range string
|
|
1666
|
+
* @returns The number of entries, capped at EXPANDED_RANGE_LIMIT like the expansion itself
|
|
1667
|
+
*/
|
|
1668
|
+
function countRange(range) {
|
|
1669
|
+
let count = 0;
|
|
1670
|
+
for (let [first, second] of parseRangeEntries(range)) {
|
|
1671
|
+
count += Math.abs(second - first) + 1;
|
|
1672
|
+
if (count >= exports.EXPANDED_RANGE_LIMIT) {
|
|
1673
|
+
return exports.EXPANDED_RANGE_LIMIT;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return count;
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Expands an IMAP sequence range string (e.g. "1:3,5,7:9") into an array of numbers.
|
|
1680
|
+
*
|
|
1681
|
+
* Entries with endpoints that are not valid nz-numbers are skipped - the input
|
|
1682
|
+
* may come from an untrusted server, and 'Infinity' or similar garbage would
|
|
1683
|
+
* otherwise loop without bound. The whole set is expanded to at most
|
|
1684
|
+
* EXPANDED_RANGE_LIMIT entries in total: legitimate responses never reach the limit
|
|
1685
|
+
* (the mailbox would need that many messages), while hostile input is cut off
|
|
1686
|
+
* instead of exhausting memory. The total is capped, not just each range -
|
|
1687
|
+
* otherwise "1:16777216,1:16777216,..." would multiply the per-range bound by an
|
|
1688
|
+
* unbounded number of ranges.
|
|
1689
|
+
*
|
|
1690
|
+
* @param range - IMAP sequence range string
|
|
1691
|
+
* @returns Array of expanded sequence numbers
|
|
1692
|
+
*/
|
|
1693
|
+
function expandRange(range) {
|
|
1694
|
+
return Array.from(iterateRange(range));
|
|
1618
1695
|
}
|
|
1619
1696
|
/**
|
|
1620
1697
|
* Returns a stream decoder for the given charset. Uses a special Japanese
|
|
@@ -1656,20 +1733,21 @@ function packMessageRange(list) {
|
|
|
1656
1733
|
// Deduplicate before sorting so that repeated values do not produce
|
|
1657
1734
|
// overlapping/non-canonical tokens (e.g. [1,1,2,3] -> "1:3", not "1,1:3").
|
|
1658
1735
|
items = Array.from(new Set(items)).sort((a, b) => a - b);
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1736
|
+
// One forward pass: a run of consecutive values is written out as soon as it ends. The runs
|
|
1737
|
+
// used to be built with unshift(), which copies the array on every call, so packing a large
|
|
1738
|
+
// SEARCH result (a server-chosen size) took seconds for 200k UIDs and about a minute for 1M,
|
|
1739
|
+
// all of it on the event loop.
|
|
1740
|
+
let parts = [];
|
|
1741
|
+
let start = items[0];
|
|
1742
|
+
let prev = start;
|
|
1743
|
+
for (let i = 1; i <= items.length; i++) {
|
|
1744
|
+
let value = items[i];
|
|
1745
|
+
if (i < items.length && value === prev + 1) {
|
|
1746
|
+
prev = value;
|
|
1664
1747
|
continue;
|
|
1665
1748
|
}
|
|
1666
|
-
|
|
1749
|
+
parts.push(start === prev ? String(start) : start + ':' + prev);
|
|
1750
|
+
start = prev = value;
|
|
1667
1751
|
}
|
|
1668
|
-
let parts = result.map(item => {
|
|
1669
|
-
if (item.length === 1) {
|
|
1670
|
-
return item[0];
|
|
1671
|
-
}
|
|
1672
|
-
return item.shift() + ':' + item.pop();
|
|
1673
|
-
});
|
|
1674
1752
|
return parts.join(',');
|
|
1675
1753
|
}
|