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,278 @@
|
|
|
1
|
+
// The `transport` option over a socket API built on web streams: the socket opens with a promise
|
|
2
|
+
// that resolves to a ReadableStream and a WritableStream of bytes. `cloudflare:sockets` (celld,
|
|
3
|
+
// Cloudflare Workers) is one, see sockets-transport.ts; Chrome's Direct Sockets and in-memory
|
|
4
|
+
// connections fit the same shape. The streams are wrapped into a Duplex that behaves like the
|
|
5
|
+
// net.Socket ImapFlow drives.
|
|
6
|
+
import { Duplex } from 'node:stream';
|
|
7
|
+
import { clearTimer, noop, unrefTimer } from './tools.js';
|
|
8
|
+
function closeQuietly(socket) {
|
|
9
|
+
try {
|
|
10
|
+
Promise.resolve(socket.close()).catch(noop);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// already closed
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
class StreamsDuplex extends Duplex {
|
|
17
|
+
constructor(api, socket, onConnect, plain) {
|
|
18
|
+
super({ allowHalfOpen: false });
|
|
19
|
+
// Taken once the socket is open: locking the streams of a socket returned by startTls()
|
|
20
|
+
// before its handshake completes stalls the handshake on celld
|
|
21
|
+
this._reader = null;
|
|
22
|
+
this._writer = null;
|
|
23
|
+
this._pumping = false;
|
|
24
|
+
this._readRequested = false;
|
|
25
|
+
// A runtime may only start TLS on a socket without a read in flight (with one, celld never
|
|
26
|
+
// completes the handshake), while the read loop always keeps one. So once the client
|
|
27
|
+
// announced a STARTTLS (holdAfterStarttls), reading stops after the tagged line that answers
|
|
28
|
+
// it, the only command in flight then, and resumes only when that line is not an OK.
|
|
29
|
+
this._awaitingStarttls = false;
|
|
30
|
+
this._lineTail = '';
|
|
31
|
+
// Set when the partial line got too long to be the one looked for and was dropped: the rest
|
|
32
|
+
// of it, up to the next line break, is skipped too
|
|
33
|
+
this._tailDropped = false;
|
|
34
|
+
this._readsHeld = false;
|
|
35
|
+
// Set once startTls() handed the connection to a TLS socket: the read that ends then is not
|
|
36
|
+
// the end of the connection, and closing this socket would close the TLS one
|
|
37
|
+
this._upgraded = false;
|
|
38
|
+
this._timeout = 0;
|
|
39
|
+
this._timer = null;
|
|
40
|
+
// Traffic only records the time, the one timer re-arms itself for the rest of the idle period
|
|
41
|
+
this._lastActivity = 0;
|
|
42
|
+
this._api = api;
|
|
43
|
+
this._socket = socket;
|
|
44
|
+
this._plain = plain || null;
|
|
45
|
+
socket.opened.then(info => {
|
|
46
|
+
if (this.destroyed) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
this._reader = info.readable.getReader();
|
|
51
|
+
this._writer = info.writable.getWriter();
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
// streams already locked by someone else
|
|
55
|
+
this.destroy(err);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.remoteAddress = info.remoteAddress;
|
|
59
|
+
this.remotePort = info.remotePort;
|
|
60
|
+
this.localAddress = info.localAddress;
|
|
61
|
+
this.localPort = info.localPort;
|
|
62
|
+
const tls = info.tls;
|
|
63
|
+
if (tls) {
|
|
64
|
+
this.authorized = tls.authorized;
|
|
65
|
+
this.getCipher = () => ({ name: tls.name || '', standardName: tls.standardName || tls.name || '', version: tls.version || '' });
|
|
66
|
+
}
|
|
67
|
+
this._touch();
|
|
68
|
+
onConnect();
|
|
69
|
+
if (this._readRequested) {
|
|
70
|
+
void this._pump();
|
|
71
|
+
}
|
|
72
|
+
}, (err) => this.destroy(err));
|
|
73
|
+
socket.closed?.catch((err) => {
|
|
74
|
+
if (!this._upgraded) {
|
|
75
|
+
this.destroy(err);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
_read() {
|
|
80
|
+
if (this._readsHeld) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!this._reader) {
|
|
84
|
+
this._readRequested = true;
|
|
85
|
+
}
|
|
86
|
+
else if (!this._pumping) {
|
|
87
|
+
void this._pump();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async _pump() {
|
|
91
|
+
const reader = this._reader;
|
|
92
|
+
this._pumping = true;
|
|
93
|
+
try {
|
|
94
|
+
for (;;) {
|
|
95
|
+
const { done, value } = await reader.read();
|
|
96
|
+
if (this._upgraded || this.destroyed) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (done || !value) {
|
|
100
|
+
this.push(null);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this._touch();
|
|
104
|
+
const chunk = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
105
|
+
const pushed = this.push(chunk);
|
|
106
|
+
if (this._awaitingStarttls && this._starttlsCompleted(chunk)) {
|
|
107
|
+
this._readsHeld = true;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!pushed) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
if (!this._upgraded) {
|
|
117
|
+
this.destroy(err);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
this._pumping = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Whether the chunk holds the tagged response of the STARTTLS command and it is an OK. Lines
|
|
125
|
+
// end with LF, CR is optional, as for the parser
|
|
126
|
+
_starttlsCompleted(chunk) {
|
|
127
|
+
const skipFirst = this._tailDropped;
|
|
128
|
+
const lines = (this._lineTail + chunk.toString('latin1')).split('\n');
|
|
129
|
+
const tail = lines.pop();
|
|
130
|
+
if (skipFirst && !lines.length) {
|
|
131
|
+
// still inside the dropped line
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
this._tailDropped = tail.length > 1024;
|
|
135
|
+
this._lineTail = this._tailDropped ? '' : tail;
|
|
136
|
+
for (let i = skipFirst ? 1 : 0; i < lines.length; i++) {
|
|
137
|
+
const tagged = /^(?![*+] )\S+ (OK|NO|BAD)\b/i.exec(lines[i]);
|
|
138
|
+
if (tagged) {
|
|
139
|
+
this._awaitingStarttls = false;
|
|
140
|
+
return tagged[1].toUpperCase() === 'OK';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
/** Stops reading after the tagged response of the STARTTLS command about to be sent, if it is an OK */
|
|
146
|
+
holdAfterStarttls() {
|
|
147
|
+
this._awaitingStarttls = true;
|
|
148
|
+
this._lineTail = '';
|
|
149
|
+
this._tailDropped = false;
|
|
150
|
+
}
|
|
151
|
+
_write(chunk, _encoding, callback) {
|
|
152
|
+
if (!this._writer) {
|
|
153
|
+
// ImapFlow writes only after onConnect, this is a misuse
|
|
154
|
+
callback(new Error('Socket is not open yet'));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this._touch();
|
|
158
|
+
this._writer.write(chunk).then(() => callback(), callback);
|
|
159
|
+
}
|
|
160
|
+
_destroy(err, callback) {
|
|
161
|
+
this._clearTimer();
|
|
162
|
+
if (!this._upgraded) {
|
|
163
|
+
this._close();
|
|
164
|
+
}
|
|
165
|
+
if (this._plain) {
|
|
166
|
+
// the plain connection went with the TLS one, unless the API keeps it open
|
|
167
|
+
this._plain._close();
|
|
168
|
+
this._plain.destroy();
|
|
169
|
+
}
|
|
170
|
+
callback(err);
|
|
171
|
+
}
|
|
172
|
+
// Closes the connection. An API that refuses to close while its streams are locked (Direct
|
|
173
|
+
// Sockets) gets a second attempt with the locks released; releasing them right away would
|
|
174
|
+
// fail the read in flight, which ends the request on celld
|
|
175
|
+
_close() {
|
|
176
|
+
const retry = () => {
|
|
177
|
+
for (const stream of [this._reader, this._writer]) {
|
|
178
|
+
try {
|
|
179
|
+
stream?.releaseLock();
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// an older streams implementation refuses with a read in flight
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
closeQuietly(this._socket);
|
|
186
|
+
};
|
|
187
|
+
try {
|
|
188
|
+
const closing = this._socket.close();
|
|
189
|
+
if (typeof closing?.then === 'function') {
|
|
190
|
+
closing.then(undefined, retry);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
retry();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Like net.Socket#setTimeout(): emits 'timeout' after `ms` without traffic, 0 disables */
|
|
198
|
+
setTimeout(ms) {
|
|
199
|
+
this._timeout = ms;
|
|
200
|
+
this._clearTimer();
|
|
201
|
+
this._touch();
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
setKeepAlive() {
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
/** Hands the connection over to a TLS socket, this one only stays as its transport */
|
|
208
|
+
startTls(options, onSecure) {
|
|
209
|
+
this._upgraded = true;
|
|
210
|
+
this._clearTimer();
|
|
211
|
+
// workerd refuses to start TLS while the streams are locked, nothing is in flight on them
|
|
212
|
+
this._reader?.releaseLock();
|
|
213
|
+
this._writer?.releaseLock();
|
|
214
|
+
let tlsSocket;
|
|
215
|
+
try {
|
|
216
|
+
tlsSocket = this._api.upgrade(this._socket, options);
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
// refused before any handshake, the connection is closed with this socket
|
|
220
|
+
this._upgraded = false;
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
return new StreamsDuplex(this._api, tlsSocket, onSecure, this);
|
|
224
|
+
}
|
|
225
|
+
_touch() {
|
|
226
|
+
this._lastActivity = Date.now();
|
|
227
|
+
if (!this._timer) {
|
|
228
|
+
this._arm(this._timeout);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
_arm(ms) {
|
|
232
|
+
if (this._timeout > 0 && !this.destroyed && !this._upgraded) {
|
|
233
|
+
this._timer = unrefTimer(setTimeout(() => {
|
|
234
|
+
this._timer = null;
|
|
235
|
+
const idle = Date.now() - this._lastActivity;
|
|
236
|
+
if (idle >= this._timeout) {
|
|
237
|
+
this.emit('timeout');
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
this._arm(this._timeout - idle);
|
|
241
|
+
}
|
|
242
|
+
}, ms));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
_clearTimer() {
|
|
246
|
+
clearTimer(this._timer);
|
|
247
|
+
this._timer = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Builds a transport for the `transport` option over a socket API built on web streams. A new
|
|
252
|
+
* API needs only `open()`, and `upgrade()` if it can start TLS on an open connection:
|
|
253
|
+
*
|
|
254
|
+
* ```js
|
|
255
|
+
* const transport = createStreamsTransport({
|
|
256
|
+
* open: ({ host, port, secure }) => {
|
|
257
|
+
* const socket = openSomeSocket(host, port, secure);
|
|
258
|
+
* return { opened: socket.ready.then(() => ({ readable: socket.readable, writable: socket.writable })), close: () => socket.close() };
|
|
259
|
+
* }
|
|
260
|
+
* });
|
|
261
|
+
* ```
|
|
262
|
+
*
|
|
263
|
+
* The transport stops reading once the server accepted a STARTTLS, so the API never has to start
|
|
264
|
+
* TLS with a read in flight, and releases the stream locks before `upgrade()` is called.
|
|
265
|
+
*
|
|
266
|
+
* @param api - Opens (and upgrades) connections
|
|
267
|
+
* @returns A transport for the `transport` option
|
|
268
|
+
*/
|
|
269
|
+
export function createStreamsTransport(api) {
|
|
270
|
+
const transport = {
|
|
271
|
+
connect: (options, onConnect) => new StreamsDuplex(api, api.open(options), onConnect)
|
|
272
|
+
};
|
|
273
|
+
if (api.upgrade) {
|
|
274
|
+
transport.prepareUpgrade = socket => socket.holdAfterStarttls();
|
|
275
|
+
transport.upgrade = (socket, options, onSecure) => socket.startTls(options, onSecure);
|
|
276
|
+
}
|
|
277
|
+
return transport;
|
|
278
|
+
}
|
package/dist/esm/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/esm/tools.js
CHANGED
|
@@ -19,6 +19,15 @@ const CONNECTION_GONE_CODES = new Set([ImapFlowErrorCode.NoConnection, ImapFlowE
|
|
|
19
19
|
// expand server sequences (e.g. ESEARCH ALL in commands/search.ts) can apply the same absolute
|
|
20
20
|
// ceiling instead of inventing their own.
|
|
21
21
|
export const EXPANDED_RANGE_LIMIT = 0x1000000;
|
|
22
|
+
// Upper bound for the untagged responses one command may accumulate in memory: the FETCH rows
|
|
23
|
+
// fetchAll() collects and the mailboxes list() collects. The response size limits bound one
|
|
24
|
+
// response at a time, not how many of them a server sends in reply to one command, so a hostile
|
|
25
|
+
// server answering a single-message FETCH with an endless stream of unsolicited FETCH rows (or a
|
|
26
|
+
// LIST with endless mailboxes) would otherwise grow the process without limit. Generous on
|
|
27
|
+
// purpose: a million rows is far beyond any legitimate listing, and a mailbox that large is read
|
|
28
|
+
// through the streaming fetch() generator, which keeps nothing. Reaching it therefore fails the
|
|
29
|
+
// command (code TooManyResponses) instead of silently truncating its result.
|
|
30
|
+
export const MAX_COLLECTED_RESPONSES = 1000000;
|
|
22
31
|
// Digit bounds for untrusted numeric values in server responses. UIDs, UIDVALIDITY and
|
|
23
32
|
// message counts are 32-bit unsigned (nz-number in the RFC 9051 grammar, so at most 10
|
|
24
33
|
// digits); MODSEQ and number64 values are 63-bit unsigned (RFC 7162, RFC 9051), at most
|
|
@@ -64,6 +73,17 @@ const IMAP4REV2_FOLDED_CAPABILITIES = new Set([
|
|
|
64
73
|
]);
|
|
65
74
|
// Deliberate no-op, used as the observer a guarded promise attaches to its own rejection.
|
|
66
75
|
export const noop = () => { };
|
|
76
|
+
/**
|
|
77
|
+
* Tells an IP literal from a hostname without `node:net`, which some runtimes do not implement.
|
|
78
|
+
* Only used to decide whether a host can be a TLS servername: a dotted quad, or anything with a
|
|
79
|
+
* colon (an IPv6 literal, a hostname never contains one).
|
|
80
|
+
*
|
|
81
|
+
* @param host - Hostname or IP address
|
|
82
|
+
* @returns true for an IP literal
|
|
83
|
+
*/
|
|
84
|
+
export function isIpLiteral(host) {
|
|
85
|
+
return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(':');
|
|
86
|
+
}
|
|
67
87
|
// The fields buildConnectionError() stamps to say *where* a connection error was rejected, as
|
|
68
88
|
// opposed to what went wrong. restampConnectionError() clears them, because they belong to the
|
|
69
89
|
// site that built the error rather than to the failure it describes.
|
|
@@ -536,13 +556,30 @@ export function getFolderTree(folders) {
|
|
|
536
556
|
root: true,
|
|
537
557
|
folders: []
|
|
538
558
|
};
|
|
559
|
+
// Children of every node indexed by name. Looking a child up with folders.find() made the
|
|
560
|
+
// tree quadratic in the number of siblings: a listing of 50k mailboxes at one level (a size
|
|
561
|
+
// the server chooses) took seconds of event loop time. A name is unique among its siblings,
|
|
562
|
+
// as the loop below updates an existing entry rather than adding a second one.
|
|
563
|
+
let childIndex = new Map();
|
|
564
|
+
let childByName = (node, name) => {
|
|
565
|
+
let index = childIndex.get(node);
|
|
566
|
+
return index ? index.get(name) : undefined;
|
|
567
|
+
};
|
|
568
|
+
let indexChild = (node, child) => {
|
|
569
|
+
let index = childIndex.get(node);
|
|
570
|
+
if (!index) {
|
|
571
|
+
index = new Map();
|
|
572
|
+
childIndex.set(node, index);
|
|
573
|
+
}
|
|
574
|
+
index.set(child.name, child);
|
|
575
|
+
};
|
|
539
576
|
let getTreeNode = (parents) => {
|
|
540
577
|
let node = tree;
|
|
541
578
|
if (!parents || !parents.length) {
|
|
542
579
|
return node;
|
|
543
580
|
}
|
|
544
581
|
for (let parent of parents) {
|
|
545
|
-
let cur = node
|
|
582
|
+
let cur = childByName(node, parent);
|
|
546
583
|
if (cur) {
|
|
547
584
|
node = cur;
|
|
548
585
|
}
|
|
@@ -557,7 +594,7 @@ export function getFolderTree(folders) {
|
|
|
557
594
|
for (let folder of byDepth) {
|
|
558
595
|
let parent = getTreeNode(folder.parent);
|
|
559
596
|
// see if entry already exists
|
|
560
|
-
let existing = parent
|
|
597
|
+
let existing = childByName(parent, folder.name);
|
|
561
598
|
if (existing) {
|
|
562
599
|
// update values
|
|
563
600
|
existing.name = folder.name;
|
|
@@ -602,6 +639,7 @@ export function getFolderTree(folders) {
|
|
|
602
639
|
parent.folders = [];
|
|
603
640
|
}
|
|
604
641
|
parent.folders.push(data);
|
|
642
|
+
indexChild(parent, data);
|
|
605
643
|
}
|
|
606
644
|
}
|
|
607
645
|
return tree;
|
|
@@ -1496,67 +1534,103 @@ export function parseUintValue(value, maxDigits) {
|
|
|
1496
1534
|
return Number.isSafeInteger(num) ? num : false;
|
|
1497
1535
|
}
|
|
1498
1536
|
/**
|
|
1499
|
-
*
|
|
1500
|
-
*
|
|
1501
|
-
* Entries with
|
|
1502
|
-
*
|
|
1503
|
-
*
|
|
1504
|
-
* EXPANDED_RANGE_LIMIT entries in total: legitimate responses never reach the limit
|
|
1505
|
-
* (the mailbox would need that many messages), while hostile input is cut off
|
|
1506
|
-
* instead of exhausting memory. The total is capped, not just each range -
|
|
1507
|
-
* otherwise "1:16777216,1:16777216,..." would multiply the per-range bound by an
|
|
1508
|
-
* unbounded number of ranges.
|
|
1537
|
+
* Parses an IMAP sequence set string (e.g. "1:3,5,9:7") into its entries, each as the pair of
|
|
1538
|
+
* endpoints in the order written: a reversed "9:7" stays reversed, and iterateRange() walks it
|
|
1539
|
+
* downwards. Entries with an endpoint that is not a valid nz-number are skipped - the input may
|
|
1540
|
+
* come from an untrusted server, and 'Infinity' or similar garbage would otherwise loop without
|
|
1541
|
+
* bound.
|
|
1509
1542
|
*
|
|
1510
|
-
* @param range - IMAP sequence
|
|
1511
|
-
* @returns
|
|
1543
|
+
* @param range - IMAP sequence set string, or whatever the response parser produced for it
|
|
1544
|
+
* @returns The validated entries
|
|
1512
1545
|
*/
|
|
1513
|
-
|
|
1514
|
-
let
|
|
1546
|
+
function parseRangeEntries(range) {
|
|
1547
|
+
let entries = [];
|
|
1515
1548
|
// Callers pass whatever the response parser produced for the sequence set, and a
|
|
1516
1549
|
// malformed response can leave that as `false` (e.g. a VANISHED response carrying
|
|
1517
1550
|
// only the (EARLIER) tag). Nothing to expand then, and throwing here would abort
|
|
1518
1551
|
// the handler for the rest of the response.
|
|
1519
1552
|
if (typeof range !== 'string') {
|
|
1520
|
-
return
|
|
1553
|
+
return entries;
|
|
1521
1554
|
}
|
|
1522
1555
|
for (let entry of range.split(',')) {
|
|
1523
|
-
if (result.length >= EXPANDED_RANGE_LIMIT) {
|
|
1524
|
-
break;
|
|
1525
|
-
}
|
|
1526
1556
|
entry = entry.trim();
|
|
1527
1557
|
let colon = entry.indexOf(':');
|
|
1528
1558
|
if (colon < 0) {
|
|
1529
1559
|
let value = Number(entry);
|
|
1530
1560
|
if (isValidSequenceValue(value)) {
|
|
1531
|
-
|
|
1561
|
+
entries.push([value, value]);
|
|
1532
1562
|
}
|
|
1533
1563
|
continue;
|
|
1534
1564
|
}
|
|
1535
1565
|
let first = Number(entry.substring(0, colon));
|
|
1536
1566
|
let second = Number(entry.slice(colon + 1));
|
|
1537
|
-
if (
|
|
1538
|
-
|
|
1539
|
-
}
|
|
1540
|
-
if (first === second) {
|
|
1541
|
-
result.push(first);
|
|
1542
|
-
continue;
|
|
1567
|
+
if (isValidSequenceValue(first) && isValidSequenceValue(second)) {
|
|
1568
|
+
entries.push([first, second]);
|
|
1543
1569
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1570
|
+
}
|
|
1571
|
+
return entries;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Walks an IMAP sequence range string (e.g. "1:3,5,7:9") one number at a time, without
|
|
1575
|
+
* materializing it. A single "1:16777216" is a few bytes on the wire but sixteen million
|
|
1576
|
+
* entries expanded, so a consumer that only needs to visit each number (the VANISHED handler)
|
|
1577
|
+
* iterates instead of calling expandRange().
|
|
1578
|
+
*
|
|
1579
|
+
* The same bound as expandRange() applies: at most EXPANDED_RANGE_LIMIT numbers in total are
|
|
1580
|
+
* produced, over all entries of the set.
|
|
1581
|
+
*
|
|
1582
|
+
* @param range - IMAP sequence range string
|
|
1583
|
+
* @returns The sequence numbers, in the order the set lists them
|
|
1584
|
+
*/
|
|
1585
|
+
export function* iterateRange(range) {
|
|
1586
|
+
let count = 0;
|
|
1587
|
+
for (let [first, second] of parseRangeEntries(range)) {
|
|
1588
|
+
let step = first <= second ? 1 : -1;
|
|
1589
|
+
for (let i = first; count < EXPANDED_RANGE_LIMIT; i += step) {
|
|
1590
|
+
yield i;
|
|
1591
|
+
count++;
|
|
1592
|
+
if (i === second) {
|
|
1593
|
+
break;
|
|
1550
1594
|
}
|
|
1551
1595
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
for (let i = first; i >= last; i--) {
|
|
1555
|
-
result.push(i);
|
|
1556
|
-
}
|
|
1596
|
+
if (count >= EXPANDED_RANGE_LIMIT) {
|
|
1597
|
+
break;
|
|
1557
1598
|
}
|
|
1558
1599
|
}
|
|
1559
|
-
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Counts the numbers iterateRange() produces for a sequence range string, without walking it.
|
|
1603
|
+
*
|
|
1604
|
+
* @param range - IMAP sequence range string
|
|
1605
|
+
* @returns The number of entries, capped at EXPANDED_RANGE_LIMIT like the expansion itself
|
|
1606
|
+
*/
|
|
1607
|
+
export function countRange(range) {
|
|
1608
|
+
let count = 0;
|
|
1609
|
+
for (let [first, second] of parseRangeEntries(range)) {
|
|
1610
|
+
count += Math.abs(second - first) + 1;
|
|
1611
|
+
if (count >= EXPANDED_RANGE_LIMIT) {
|
|
1612
|
+
return EXPANDED_RANGE_LIMIT;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
return count;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Expands an IMAP sequence range string (e.g. "1:3,5,7:9") into an array of numbers.
|
|
1619
|
+
*
|
|
1620
|
+
* Entries with endpoints that are not valid nz-numbers are skipped - the input
|
|
1621
|
+
* may come from an untrusted server, and 'Infinity' or similar garbage would
|
|
1622
|
+
* otherwise loop without bound. The whole set is expanded to at most
|
|
1623
|
+
* EXPANDED_RANGE_LIMIT entries in total: legitimate responses never reach the limit
|
|
1624
|
+
* (the mailbox would need that many messages), while hostile input is cut off
|
|
1625
|
+
* instead of exhausting memory. The total is capped, not just each range -
|
|
1626
|
+
* otherwise "1:16777216,1:16777216,..." would multiply the per-range bound by an
|
|
1627
|
+
* unbounded number of ranges.
|
|
1628
|
+
*
|
|
1629
|
+
* @param range - IMAP sequence range string
|
|
1630
|
+
* @returns Array of expanded sequence numbers
|
|
1631
|
+
*/
|
|
1632
|
+
export function expandRange(range) {
|
|
1633
|
+
return Array.from(iterateRange(range));
|
|
1560
1634
|
}
|
|
1561
1635
|
/**
|
|
1562
1636
|
* Returns a stream decoder for the given charset. Uses a special Japanese
|
|
@@ -1598,20 +1672,21 @@ export function packMessageRange(list) {
|
|
|
1598
1672
|
// Deduplicate before sorting so that repeated values do not produce
|
|
1599
1673
|
// overlapping/non-canonical tokens (e.g. [1,1,2,3] -> "1:3", not "1,1:3").
|
|
1600
1674
|
items = Array.from(new Set(items)).sort((a, b) => a - b);
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1675
|
+
// One forward pass: a run of consecutive values is written out as soon as it ends. The runs
|
|
1676
|
+
// used to be built with unshift(), which copies the array on every call, so packing a large
|
|
1677
|
+
// SEARCH result (a server-chosen size) took seconds for 200k UIDs and about a minute for 1M,
|
|
1678
|
+
// all of it on the event loop.
|
|
1679
|
+
let parts = [];
|
|
1680
|
+
let start = items[0];
|
|
1681
|
+
let prev = start;
|
|
1682
|
+
for (let i = 1; i <= items.length; i++) {
|
|
1683
|
+
let value = items[i];
|
|
1684
|
+
if (i < items.length && value === prev + 1) {
|
|
1685
|
+
prev = value;
|
|
1606
1686
|
continue;
|
|
1607
1687
|
}
|
|
1608
|
-
|
|
1688
|
+
parts.push(start === prev ? String(start) : start + ':' + prev);
|
|
1689
|
+
start = prev = value;
|
|
1609
1690
|
}
|
|
1610
|
-
let parts = result.map(item => {
|
|
1611
|
-
if (item.length === 1) {
|
|
1612
|
-
return item[0];
|
|
1613
|
-
}
|
|
1614
|
-
return item.shift() + ':' + item.pop();
|
|
1615
|
-
});
|
|
1616
1691
|
return parts.join(',');
|
|
1617
1692
|
}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConnectionOptions, CipherNameAndProtocol } from 'node:tls';
|
|
2
|
-
import type { Readable } from 'node:stream';
|
|
2
|
+
import type { Duplex, Readable } from 'node:stream';
|
|
3
3
|
/**
|
|
4
4
|
* Logger interface. Any object with `debug`, `info`, `warn` and `error` methods can be used,
|
|
5
5
|
* for example a pino or bunyan instance. `trace` and `fatal` are used when present.
|
|
@@ -95,6 +95,14 @@ export interface ImapFlowOptions {
|
|
|
95
95
|
* Node as-is. Proxy DNS and negotiation run inside `connectionTimeout`.
|
|
96
96
|
*/
|
|
97
97
|
proxy?: string | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Opens the connection through this transport instead of `node:net` and `node:tls`, for
|
|
100
|
+
* runtimes without those modules. `createSocketsTransport()` builds one from the
|
|
101
|
+
* `connect()` of the `cloudflare:sockets` API (celld, Cloudflare Workers),
|
|
102
|
+
* `createStreamsTransport()` from any socket API built on web streams. Can not be combined
|
|
103
|
+
* with `proxy`.
|
|
104
|
+
*/
|
|
105
|
+
transport?: ImapTransport | undefined;
|
|
98
106
|
/** If true, then use QRESYNC instead of CONDSTORE. EXPUNGE notifications will include UID instead of sequence number */
|
|
99
107
|
qresync?: boolean | undefined;
|
|
100
108
|
/** If set, then breaks and restarts IDLE every maxIdleTime ms */
|
|
@@ -162,7 +170,8 @@ export interface ImapFlowOptions {
|
|
|
162
170
|
* when the server offers STARTTLS and otherwise continues in cleartext with a warning logged:
|
|
163
171
|
* the capability list that decides this was itself received in cleartext, so an attacker on
|
|
164
172
|
* the path can strip it. Set `doSTARTTLS: true` whenever the credentials must never cross the
|
|
165
|
-
* wire unencrypted.
|
|
173
|
+
* wire unencrypted. STARTTLS is only possible before authentication, so a server greeting
|
|
174
|
+
* with PREAUTH is a connection failure when the upgrade is required.
|
|
166
175
|
*/
|
|
167
176
|
doSTARTTLS?: boolean | undefined;
|
|
168
177
|
/** Custom instance ID string for logs */
|
|
@@ -882,3 +891,70 @@ export interface ImapFlowEvents {
|
|
|
882
891
|
/** The server sent an ALERT: text meant for the user, such as a planned shutdown or an account problem */
|
|
883
892
|
alert: [alert: AlertEvent];
|
|
884
893
|
}
|
|
894
|
+
/** Where {@link ImapTransport} connects to, and the `tls` options of the client */
|
|
895
|
+
export interface ImapTransportOptions {
|
|
896
|
+
host: string;
|
|
897
|
+
port: number;
|
|
898
|
+
/** The SNI name, absent for an IP literal host without the `servername` option */
|
|
899
|
+
servername?: string | undefined;
|
|
900
|
+
/** The `tls` option of the client */
|
|
901
|
+
tls?: ConnectionOptions | undefined;
|
|
902
|
+
}
|
|
903
|
+
/** What {@link ImapTransport.connect} gets: the destination, and whether to open a TLS connection */
|
|
904
|
+
export interface ImapTransportConnectOptions extends ImapTransportOptions {
|
|
905
|
+
/** Open a TLS connection (the `secure` option), otherwise a cleartext one */
|
|
906
|
+
secure: boolean;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* The socket a {@link ImapTransport} returns: a Duplex stream of Buffers that emits `error`,
|
|
910
|
+
* `end` and `close`, and `timeout` once `setTimeout(ms)` was called and `ms` passed without
|
|
911
|
+
* traffic (0 disables it, like `net.Socket#setTimeout()`), the watchdog behind the
|
|
912
|
+
* `socketTimeout` option. The rest of what a `net.Socket` has is optional; ImapFlow uses it when
|
|
913
|
+
* it is there (the addresses for logging, the cipher of a TLS connection for `client.tls`).
|
|
914
|
+
*/
|
|
915
|
+
export type ImapTransportSocket = Duplex & {
|
|
916
|
+
setTimeout(ms: number): unknown;
|
|
917
|
+
setKeepAlive?: ((enable?: boolean, initialDelay?: number) => unknown) | undefined;
|
|
918
|
+
remoteAddress?: string | undefined;
|
|
919
|
+
remotePort?: number | undefined;
|
|
920
|
+
localAddress?: string | undefined;
|
|
921
|
+
localPort?: number | undefined;
|
|
922
|
+
getCipher?: (() => CipherNameAndProtocol | null) | undefined;
|
|
923
|
+
authorized?: boolean | undefined;
|
|
924
|
+
};
|
|
925
|
+
/**
|
|
926
|
+
* Opens and upgrades the socket carrying the session, see the `transport` option.
|
|
927
|
+
* `createStreamsTransport()` builds one over any socket API built on web streams,
|
|
928
|
+
* `createSocketsTransport()` over `cloudflare:sockets`.
|
|
929
|
+
*/
|
|
930
|
+
export interface ImapTransport {
|
|
931
|
+
/**
|
|
932
|
+
* Opens a connection, cleartext or TLS. Connection errors are emitted on the returned
|
|
933
|
+
* socket; a connection refused up front (an option the transport can not honor) may throw.
|
|
934
|
+
*
|
|
935
|
+
* @param options - Destination, TLS options and whether to open a TLS connection
|
|
936
|
+
* @param onConnect - Called once the connection (and the TLS handshake) is established
|
|
937
|
+
*/
|
|
938
|
+
connect(options: ImapTransportConnectOptions, onConnect: () => void): ImapTransportSocket;
|
|
939
|
+
/**
|
|
940
|
+
* Called right before the STARTTLS command is sent on the cleartext socket, so a transport
|
|
941
|
+
* that can only start TLS without a read in flight can stop reading once it is answered.
|
|
942
|
+
* STARTTLS is the only command in flight then: the next tagged response line (one that does
|
|
943
|
+
* not start with `* ` or `+ `) answers it, and if it is an OK, the server sends nothing more
|
|
944
|
+
* before the TLS handshake.
|
|
945
|
+
*
|
|
946
|
+
* @param socket - The cleartext socket returned by connect()
|
|
947
|
+
*/
|
|
948
|
+
prepareUpgrade?: ((socket: ImapTransportSocket) => void) | undefined;
|
|
949
|
+
/**
|
|
950
|
+
* Starts TLS on a connected cleartext socket after STARTTLS. May throw if the upgrade is
|
|
951
|
+
* refused before any handshake, handshake errors are emitted on the returned socket. A
|
|
952
|
+
* transport that can not start TLS leaves it out: ImapFlow then treats the server as one
|
|
953
|
+
* without STARTTLS (see the `doSTARTTLS` option).
|
|
954
|
+
*
|
|
955
|
+
* @param socket - The cleartext socket returned by connect()
|
|
956
|
+
* @param options - Destination and TLS options
|
|
957
|
+
* @param onSecure - Called once the TLS handshake is complete
|
|
958
|
+
*/
|
|
959
|
+
upgrade?: ((socket: ImapTransportSocket, options: ImapTransportOptions, onSecure: () => void) => ImapTransportSocket) | undefined;
|
|
960
|
+
}
|