imapflow 2.5.8 → 2.6.0
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 +26 -0
- package/README.md +9 -0
- package/dist/cjs/commands/append.js +6 -3
- package/dist/cjs/commands/fetch.d.ts +2 -1
- package/dist/cjs/commands/fetch.js +8 -2
- package/dist/cjs/commands/idle.js +7 -0
- package/dist/cjs/commands/select.js +17 -6
- package/dist/cjs/commands/status.js +19 -5
- package/dist/cjs/download.js +3 -3
- package/dist/cjs/errors.d.ts +3 -1
- package/dist/cjs/errors.js +4 -1
- package/dist/cjs/handler/imap-parser.js +38 -6
- package/dist/cjs/handler/limits.d.ts +1 -0
- package/dist/cjs/handler/limits.js +8 -1
- package/dist/cjs/handler/token-parser.d.ts +8 -2
- package/dist/cjs/handler/token-parser.js +55 -16
- package/dist/cjs/handler/types.d.ts +5 -2
- package/dist/cjs/imap-flow.d.ts +38 -2
- package/dist/cjs/imap-flow.js +99 -15
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/types.d.ts +51 -7
- package/dist/esm/commands/append.js +6 -3
- package/dist/esm/commands/fetch.d.ts +2 -1
- package/dist/esm/commands/fetch.js +8 -2
- package/dist/esm/commands/idle.js +7 -0
- package/dist/esm/commands/select.js +17 -6
- package/dist/esm/commands/status.js +19 -5
- package/dist/esm/download.js +3 -3
- package/dist/esm/errors.d.ts +3 -1
- package/dist/esm/errors.js +4 -1
- package/dist/esm/handler/imap-parser.js +38 -6
- package/dist/esm/handler/limits.d.ts +1 -0
- package/dist/esm/handler/limits.js +7 -0
- package/dist/esm/handler/token-parser.d.ts +8 -2
- package/dist/esm/handler/token-parser.js +56 -17
- package/dist/esm/handler/types.d.ts +5 -2
- package/dist/esm/imap-flow.d.ts +38 -2
- package/dist/esm/imap-flow.js +99 -15
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/types.d.ts +51 -7
- package/package.json +3 -3
package/dist/cjs/imap-flow.d.ts
CHANGED
|
@@ -134,6 +134,23 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
134
134
|
* Is current mailbox idling (`true`) or not (`false`)
|
|
135
135
|
*/
|
|
136
136
|
idling: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Data of the application's own, kept with the client and never read by ImapFlow. Starts as
|
|
139
|
+
* the `context` option; cast it to the type the application stores there
|
|
140
|
+
*/
|
|
141
|
+
context: unknown;
|
|
142
|
+
/**
|
|
143
|
+
* When the TCP (or TLS) connection to the server was established, in milliseconds since the
|
|
144
|
+
* epoch, `null` before that. Kept after the connection closed
|
|
145
|
+
*/
|
|
146
|
+
connectedAt: number | null;
|
|
147
|
+
/**
|
|
148
|
+
* When the client last wrote to the server (a command, a literal, the DONE that ends IDLE, an
|
|
149
|
+
* authentication response), in milliseconds since the epoch, `null` before the first write.
|
|
150
|
+
* Servers drop sessions that have not sent anything for a while, the IDLE ones included
|
|
151
|
+
* (RFC 9051 allows 30 minutes), and this is what their timer measures
|
|
152
|
+
*/
|
|
153
|
+
lastClientWrite: number | null;
|
|
137
154
|
/** Whether log entries are also emitted as 'log' events, see the `emitLogs` option */
|
|
138
155
|
emitLogs: boolean;
|
|
139
156
|
/** Hash algorithm for the fallback message id, see `ImapFlowOptions.idHashAlgorithm` */
|
|
@@ -148,6 +165,16 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
148
165
|
byeReason: string | undefined;
|
|
149
166
|
/** Negotiated TLS session details, `false` for a cleartext connection */
|
|
150
167
|
tls: TlsInfo | false | undefined;
|
|
168
|
+
/**
|
|
169
|
+
* The mailboxes a `verifyOnly` connection with `includeMailboxes` listed before it logged out,
|
|
170
|
+
* `undefined` otherwise
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* let client = new ImapFlow({ ..., verifyOnly: true, includeMailboxes: true });
|
|
174
|
+
* await client.connect();
|
|
175
|
+
* console.log(client.mailboxList.map(mailbox => mailbox.path));
|
|
176
|
+
*/
|
|
177
|
+
get mailboxList(): ListResponse[] | undefined;
|
|
151
178
|
/**
|
|
152
179
|
* Creates a client for one IMAP connection. Nothing is sent before `connect()` is called
|
|
153
180
|
*/
|
|
@@ -344,6 +371,9 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
344
371
|
* otherwise IDLE is started by default on connection inactivity. NB! If `idle()` is called manually then it does not
|
|
345
372
|
* return until IDLE is finished which means you would have to call some other command out of scope.
|
|
346
373
|
*
|
|
374
|
+
* Unlike auto-IDLE, a direct call does not wait for the connection to be free: a mailbox lock
|
|
375
|
+
* holder may idle inside its own lock, and idle() can not tell it from another caller.
|
|
376
|
+
*
|
|
347
377
|
* @returns `false` if IDLE failed, `undefined` otherwise
|
|
348
378
|
*
|
|
349
379
|
* @example
|
|
@@ -505,6 +535,9 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
505
535
|
* @param query Fetch query
|
|
506
536
|
* @param options Fetch options
|
|
507
537
|
* @yields Message data object
|
|
538
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected (for example
|
|
539
|
+
* after `mailboxClose()` or a failed `mailboxOpen()`), or `NoConnection` once the connection
|
|
540
|
+
* is gone. Earlier releases yielded nothing in that case, as for an empty mailbox
|
|
508
541
|
*
|
|
509
542
|
* @example
|
|
510
543
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -526,6 +559,7 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
526
559
|
* @param query Fetch query
|
|
527
560
|
* @param options Fetch options
|
|
528
561
|
* @returns Array of Message data object
|
|
562
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch()
|
|
529
563
|
*
|
|
530
564
|
* @example
|
|
531
565
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -542,7 +576,9 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
542
576
|
* @param seq Single UID or sequence number of the message to fetch for
|
|
543
577
|
* @param query Fetch query
|
|
544
578
|
* @param options Fetch options
|
|
545
|
-
* @returns Message data object
|
|
579
|
+
* @returns Message data object, `false` when the message was not found
|
|
580
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch().
|
|
581
|
+
* Earlier releases resolved `undefined` then
|
|
546
582
|
*
|
|
547
583
|
* @example
|
|
548
584
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -550,7 +586,7 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
550
586
|
* let lastMsg = await client.fetchOne('*', {uid: true})
|
|
551
587
|
* console.log(lastMsg.uid);
|
|
552
588
|
*/
|
|
553
|
-
fetchOne(seq: SequenceString, query: FetchQueryObject, options?: FetchOptions | undefined): Promise<FetchMessageObject | false
|
|
589
|
+
fetchOne(seq: SequenceString, query: FetchQueryObject, options?: FetchOptions | undefined): Promise<FetchMessageObject | false>;
|
|
554
590
|
/**
|
|
555
591
|
* Download either full rfc822 formatted message or a specific bodystructure part as a Stream.
|
|
556
592
|
* Bodystructure parts are decoded so the resulting stream is a binary file. Text content
|
package/dist/cjs/imap-flow.js
CHANGED
|
@@ -211,6 +211,18 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
211
211
|
* Current module version as a static class property
|
|
212
212
|
*/
|
|
213
213
|
static { this.version = packageInfo.version; }
|
|
214
|
+
/**
|
|
215
|
+
* The mailboxes a `verifyOnly` connection with `includeMailboxes` listed before it logged out,
|
|
216
|
+
* `undefined` otherwise
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* let client = new ImapFlow({ ..., verifyOnly: true, includeMailboxes: true });
|
|
220
|
+
* await client.connect();
|
|
221
|
+
* console.log(client.mailboxList.map(mailbox => mailbox.path));
|
|
222
|
+
*/
|
|
223
|
+
get mailboxList() {
|
|
224
|
+
return this._mailboxList;
|
|
225
|
+
}
|
|
214
226
|
/**
|
|
215
227
|
* Creates a client for one IMAP connection. Nothing is sent before `connect()` is called
|
|
216
228
|
*/
|
|
@@ -262,6 +274,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
262
274
|
this._throttleWaits = new Set();
|
|
263
275
|
this._upgradeReject = null;
|
|
264
276
|
this.isClosed = false;
|
|
277
|
+
this._errorReported = false;
|
|
265
278
|
this.states = states;
|
|
266
279
|
this.state = this.states.NOT_AUTHENTICATED;
|
|
267
280
|
this.lockCounter = 0;
|
|
@@ -286,6 +299,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
286
299
|
this.mailbox = false;
|
|
287
300
|
this.currentSelectCommand = false;
|
|
288
301
|
this.idling = false;
|
|
302
|
+
this.context = this.options.context;
|
|
303
|
+
this.connectedAt = null;
|
|
304
|
+
this.lastClientWrite = null;
|
|
289
305
|
this.emitLogs = !!this.options.emitLogs;
|
|
290
306
|
this.lo = 0;
|
|
291
307
|
this.untaggedHandlers = {};
|
|
@@ -331,6 +347,15 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
331
347
|
return;
|
|
332
348
|
}
|
|
333
349
|
err._connId = err._connId || this.id;
|
|
350
|
+
// One error per connection: emitError() closes the connection, and until the deferred
|
|
351
|
+
// close() runs a second socket, deflate or parser error may still arrive. Emitting it as
|
|
352
|
+
// well would reach a consumer that removed its listener after the first one, or one that
|
|
353
|
+
// only awaited connect(), as an unhandled 'error' that throws.
|
|
354
|
+
if (this._errorReported) {
|
|
355
|
+
this.log.debug({ msg: 'Suppressed error after the connection already reported one', err, cid: this.id });
|
|
356
|
+
this.closeAfter();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
334
359
|
// During a STARTTLS handshake the upgrade owns the single error path (its settle()
|
|
335
360
|
// helper). Route the error there so a streamer-originated failure is surfaced with its
|
|
336
361
|
// real code (instead of a generic ClosedAfterConnect*) and cannot hang a verifyOnly
|
|
@@ -341,6 +366,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
341
366
|
this._upgradeReject = null;
|
|
342
367
|
if (typeof reject === 'function') {
|
|
343
368
|
// settle() clears the upgrade timer and flags, and closes the connection
|
|
369
|
+
this._errorReported = true;
|
|
344
370
|
reject(err);
|
|
345
371
|
return;
|
|
346
372
|
}
|
|
@@ -355,10 +381,12 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
355
381
|
let reject = this.initialReject;
|
|
356
382
|
this.initialResolve = false;
|
|
357
383
|
this.initialReject = false;
|
|
384
|
+
this._errorReported = true;
|
|
358
385
|
this.closeAfter();
|
|
359
386
|
reject(err);
|
|
360
387
|
return;
|
|
361
388
|
}
|
|
389
|
+
this._errorReported = true;
|
|
362
390
|
this.closeAfter();
|
|
363
391
|
this.emit('error', err);
|
|
364
392
|
}
|
|
@@ -412,6 +440,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
412
440
|
else {
|
|
413
441
|
return false;
|
|
414
442
|
}
|
|
443
|
+
this.lastClientWrite = Date.now();
|
|
415
444
|
for (let data of frames) {
|
|
416
445
|
if (this.logRaw) {
|
|
417
446
|
// Client frames of an authentication exchange carry credentials: the LOGIN
|
|
@@ -870,9 +899,17 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
870
899
|
async handleResponse(data) {
|
|
871
900
|
let parsed;
|
|
872
901
|
try {
|
|
873
|
-
parsed = await (0, imap_handler_js_1.parser)(data.payload, { literals: data.literals });
|
|
902
|
+
parsed = await (0, imap_handler_js_1.parser)(data.payload, { literals: data.literals, maxResponseTokens: this.options.maxResponseTokens });
|
|
874
903
|
}
|
|
875
904
|
catch (err) {
|
|
905
|
+
if (err.code === 'TooManyTokens') {
|
|
906
|
+
// A size limit, so as terminal as the ones ImapStream enforces: the server is
|
|
907
|
+
// flooding responses, and skipping the line would only invite the next one
|
|
908
|
+
this.log.error({ msg: 'Response exceeded the token limit', payloadBytes: data.payload.length, err, cid: this.id });
|
|
909
|
+
this.rejectCurrentRequest(err);
|
|
910
|
+
this.failProtocol(err);
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
876
913
|
// can not make sense of this. The payload can be up to the configured line
|
|
877
914
|
// cap (1GB by default), so log only a bounded prefix: a server looping
|
|
878
915
|
// unparseable garbage would otherwise turn this error log into a disk filler.
|
|
@@ -1294,7 +1331,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1294
1331
|
}
|
|
1295
1332
|
});
|
|
1296
1333
|
const socket = this.socket;
|
|
1297
|
-
|
|
1334
|
+
// on(), not once(): with compression the deflate and writeSocket errors are forwarded
|
|
1335
|
+
// onto this socket, and a second one before close() must not find it without a listener
|
|
1336
|
+
socket.on('error', this._socketError);
|
|
1298
1337
|
socket.once('close', this._socketClose);
|
|
1299
1338
|
socket.once('end', this._socketEnd);
|
|
1300
1339
|
socket.on('tlsClientError', this._socketError);
|
|
@@ -2273,6 +2312,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2273
2312
|
}
|
|
2274
2313
|
try {
|
|
2275
2314
|
(0, tools_js_1.clearTimer)(this.connectTimeout);
|
|
2315
|
+
this.connectedAt = Date.now();
|
|
2276
2316
|
// ImapFlow now owns the socket; drop the proxy's early error handler
|
|
2277
2317
|
// (its "before connection setup" message no longer applies).
|
|
2278
2318
|
(0, proxy_connection_js_1.detachEarlyErrorHandler)(socket);
|
|
@@ -2398,6 +2438,15 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2398
2438
|
}
|
|
2399
2439
|
return error;
|
|
2400
2440
|
}
|
|
2441
|
+
// What a message command called without a selected mailbox rejects with: NoMailboxSelected,
|
|
2442
|
+
// or NoConnection once the connection is gone, as that is why the mailbox is missing then
|
|
2443
|
+
/** @internal */
|
|
2444
|
+
createNoMailboxError(command) {
|
|
2445
|
+
if (!this.usable) {
|
|
2446
|
+
return this.createNoConnectionError(this.byeReason, { rejectedFrom: 'noMailbox', command });
|
|
2447
|
+
}
|
|
2448
|
+
return (0, errors_js_1.createImapError)('No mailbox is selected', 'NoMailboxSelected', { command });
|
|
2449
|
+
}
|
|
2401
2450
|
/**
|
|
2402
2451
|
* Closes TCP connection without notifying the server.
|
|
2403
2452
|
*
|
|
@@ -2838,8 +2887,14 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2838
2887
|
// LIST. This sets mailboxMissing on the error so the caller can distinguish
|
|
2839
2888
|
// "doesn't exist" from other failures, for getMailboxLock() callers as well.
|
|
2840
2889
|
try {
|
|
2841
|
-
let
|
|
2842
|
-
|
|
2890
|
+
let normalized = (0, tools_js_1.normalizePath)(this, path);
|
|
2891
|
+
let folders = await this.run('LIST', '', normalized, { listOnly: true });
|
|
2892
|
+
// The path is the LIST pattern, so a % or * in a name also matches other
|
|
2893
|
+
// mailboxes, and an entry marked \NonExistent (RFC 5258) does not exist either
|
|
2894
|
+
let exists = Array.isArray(folders) &&
|
|
2895
|
+
folders.some(entry => (0, tools_js_1.comparePaths)(this, entry.path, normalized) &&
|
|
2896
|
+
![...(entry.flags || [])].some(flag => typeof flag === 'string' && flag.toUpperCase() === '\\NONEXISTENT'));
|
|
2897
|
+
if (!exists) {
|
|
2843
2898
|
err.mailboxMissing = true;
|
|
2844
2899
|
}
|
|
2845
2900
|
}
|
|
@@ -2882,6 +2937,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2882
2937
|
* otherwise IDLE is started by default on connection inactivity. NB! If `idle()` is called manually then it does not
|
|
2883
2938
|
* return until IDLE is finished which means you would have to call some other command out of scope.
|
|
2884
2939
|
*
|
|
2940
|
+
* Unlike auto-IDLE, a direct call does not wait for the connection to be free: a mailbox lock
|
|
2941
|
+
* holder may idle inside its own lock, and idle() can not tell it from another caller.
|
|
2942
|
+
*
|
|
2885
2943
|
* @returns `false` if IDLE failed, `undefined` otherwise
|
|
2886
2944
|
*
|
|
2887
2945
|
* @example
|
|
@@ -3135,6 +3193,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3135
3193
|
* @param query Fetch query
|
|
3136
3194
|
* @param options Fetch options
|
|
3137
3195
|
* @yields Message data object
|
|
3196
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected (for example
|
|
3197
|
+
* after `mailboxClose()` or a failed `mailboxOpen()`), or `NoConnection` once the connection
|
|
3198
|
+
* is gone. Earlier releases yielded nothing in that case, as for an empty mailbox
|
|
3138
3199
|
*
|
|
3139
3200
|
* @example
|
|
3140
3201
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -3148,8 +3209,8 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3148
3209
|
async *fetch(range, query, options) {
|
|
3149
3210
|
options = options || {};
|
|
3150
3211
|
if (!this.mailbox) {
|
|
3151
|
-
//
|
|
3152
|
-
|
|
3212
|
+
// an empty result would look like an empty mailbox, for example right after close()
|
|
3213
|
+
throw this.createNoMailboxError('FETCH');
|
|
3153
3214
|
}
|
|
3154
3215
|
let resolved = await this.resolveRange(range, options);
|
|
3155
3216
|
if (!resolved) {
|
|
@@ -3259,6 +3320,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3259
3320
|
* @param query Fetch query
|
|
3260
3321
|
* @param options Fetch options
|
|
3261
3322
|
* @returns Array of Message data object
|
|
3323
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch()
|
|
3262
3324
|
*
|
|
3263
3325
|
* @example
|
|
3264
3326
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -3282,7 +3344,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3282
3344
|
* @param seq Single UID or sequence number of the message to fetch for
|
|
3283
3345
|
* @param query Fetch query
|
|
3284
3346
|
* @param options Fetch options
|
|
3285
|
-
* @returns Message data object
|
|
3347
|
+
* @returns Message data object, `false` when the message was not found
|
|
3348
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch().
|
|
3349
|
+
* Earlier releases resolved `undefined` then
|
|
3286
3350
|
*
|
|
3287
3351
|
* @example
|
|
3288
3352
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -3292,8 +3356,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3292
3356
|
*/
|
|
3293
3357
|
async fetchOne(seq, query, options) {
|
|
3294
3358
|
if (!this.mailbox) {
|
|
3295
|
-
//
|
|
3296
|
-
|
|
3359
|
+
// undefined used to be returned here, indistinguishable from a missing message for a
|
|
3360
|
+
// caller that only tests the result
|
|
3361
|
+
throw this.createNoMailboxError('FETCH');
|
|
3297
3362
|
}
|
|
3298
3363
|
if (seq === '*') {
|
|
3299
3364
|
if (!this.mailbox.exists) {
|
|
@@ -3471,6 +3536,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3471
3536
|
return;
|
|
3472
3537
|
}
|
|
3473
3538
|
this.processingLock = true;
|
|
3539
|
+
// The lock taken off the queue and not settled yet. A pass that throws out of its loop
|
|
3540
|
+
// rejects it, its caller would wait forever otherwise: it is in the queue no longer.
|
|
3541
|
+
let unsettled = null;
|
|
3474
3542
|
try {
|
|
3475
3543
|
// Process all locks in queue until empty
|
|
3476
3544
|
let processedCount = 0;
|
|
@@ -3487,6 +3555,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3487
3555
|
await new Promise(resolve => setImmediate(resolve));
|
|
3488
3556
|
}
|
|
3489
3557
|
const lock = this.locks.shift();
|
|
3558
|
+
unsettled = lock;
|
|
3490
3559
|
const { resolve, reject, path, options, lockId } = lock;
|
|
3491
3560
|
// From here on the grant/reject path owns the outcome; the acquire
|
|
3492
3561
|
// timer must not race with resolution.
|
|
@@ -3552,6 +3621,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3552
3621
|
};
|
|
3553
3622
|
if (!this.usable || !this.socket || this.socket.destroyed) {
|
|
3554
3623
|
this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId, idling: this.idling });
|
|
3624
|
+
unsettled = null;
|
|
3555
3625
|
reject(this.createNoConnectionError(false, { rejectedFrom: 'mailboxLock', path }));
|
|
3556
3626
|
continue; // Process next lock in queue
|
|
3557
3627
|
}
|
|
@@ -3564,6 +3634,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3564
3634
|
this.currentLock = lock;
|
|
3565
3635
|
armHeldTimer();
|
|
3566
3636
|
this.autoidle();
|
|
3637
|
+
unsettled = null;
|
|
3567
3638
|
resolve({ path, release });
|
|
3568
3639
|
};
|
|
3569
3640
|
if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
|
|
@@ -3606,23 +3677,35 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3606
3677
|
...(options.description && { description: options.description }),
|
|
3607
3678
|
err
|
|
3608
3679
|
});
|
|
3680
|
+
unsettled = null;
|
|
3609
3681
|
reject(err);
|
|
3610
3682
|
// Continue to next lock in queue
|
|
3611
3683
|
}
|
|
3612
3684
|
}
|
|
3613
3685
|
}
|
|
3686
|
+
catch (err) {
|
|
3687
|
+
if (unsettled) {
|
|
3688
|
+
let lock = unsettled;
|
|
3689
|
+
unsettled = null;
|
|
3690
|
+
// granted but not handed out (the grant itself failed): nobody can release it
|
|
3691
|
+
if (this.currentLock === lock) {
|
|
3692
|
+
(0, tools_js_1.clearTimer)(lock.heldWarnTimer);
|
|
3693
|
+
lock.heldWarnTimer = null;
|
|
3694
|
+
this.currentLock = false;
|
|
3695
|
+
}
|
|
3696
|
+
lock.reject(err);
|
|
3697
|
+
}
|
|
3698
|
+
throw err;
|
|
3699
|
+
}
|
|
3614
3700
|
finally {
|
|
3615
3701
|
this.processingLock = false;
|
|
3616
|
-
//
|
|
3617
|
-
//
|
|
3618
|
-
// arrived before we finished). Schedule another run if needed.
|
|
3619
|
-
/* c8 ignore start */ // requires a lock to be enqueued during an in-flight processLocks pass; not reproducible deterministically
|
|
3702
|
+
// A pass that ended with locks queued and none held (it threw out of its loop) would
|
|
3703
|
+
// leave them waiting with nothing to start another pass. Schedule one.
|
|
3620
3704
|
if (this.locks.length && !this.currentLock) {
|
|
3621
3705
|
setImmediate(() => {
|
|
3622
3706
|
this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
|
|
3623
3707
|
});
|
|
3624
3708
|
}
|
|
3625
|
-
/* c8 ignore stop */
|
|
3626
3709
|
}
|
|
3627
3710
|
}
|
|
3628
3711
|
/**
|
|
@@ -3817,7 +3900,8 @@ exports.ImapFlow = ImapFlow;
|
|
|
3817
3900
|
*/
|
|
3818
3901
|
/**
|
|
3819
3902
|
* Error event. In most cases getting an error event also means that connection is closed
|
|
3820
|
-
* and pending operations should return with a failure.
|
|
3903
|
+
* and pending operations should return with a failure. Emitted at most once per connection,
|
|
3904
|
+
* later errors while the connection is closing are only logged.
|
|
3821
3905
|
*
|
|
3822
3906
|
* @event ImapFlow#error
|
|
3823
3907
|
* @example
|
package/dist/cjs/package-info.js
CHANGED
package/dist/cjs/types.d.ts
CHANGED
|
@@ -76,8 +76,13 @@ export interface ImapFlowOptions {
|
|
|
76
76
|
emitLogs?: boolean | undefined;
|
|
77
77
|
/** If true, then logs out automatically after successful authentication */
|
|
78
78
|
verifyOnly?: boolean | undefined;
|
|
79
|
-
/** If true and verifyOnly is set, lists mailboxes */
|
|
79
|
+
/** If true and verifyOnly is set, lists mailboxes. The listing is in `client.mailboxList` once `connect()` resolved */
|
|
80
80
|
includeMailboxes?: boolean | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Data of the application's own to keep with the client, such as the account the connection
|
|
83
|
+
* belongs to. ImapFlow never reads it, it is the initial value of `client.context`
|
|
84
|
+
*/
|
|
85
|
+
context?: unknown;
|
|
81
86
|
/**
|
|
82
87
|
* Proxy URL. Supports HTTP CONNECT (http:, https:) and SOCKS (socks:, socks4:, socks4a:,
|
|
83
88
|
* socks5:) proxies. IPv6 proxy endpoints are given in URL form, e.g. `socks5://[2001:db8::1]:1080`.
|
|
@@ -133,8 +138,9 @@ export interface ImapFlowOptions {
|
|
|
133
138
|
* whatever is left of `maxResponseSize`, so lowering that also bounds line buffering.
|
|
134
139
|
* Exceeding it is terminal: the connection fails with error code `LineTooLarge` and
|
|
135
140
|
* no further input is parsed. The limit bounds the bytes of a line, not the memory its parse
|
|
136
|
-
* takes, which is several times the line size for a line of many small tokens
|
|
137
|
-
* answer of plain numbers is read into an array of about the
|
|
141
|
+
* takes, which is several times the line size for a line of many small tokens, see
|
|
142
|
+
* `maxResponseTokens`. A SEARCH answer of plain numbers is read into an array of about the
|
|
143
|
+
* size of the line.
|
|
138
144
|
*/
|
|
139
145
|
maxLineLength?: number | undefined;
|
|
140
146
|
/**
|
|
@@ -159,6 +165,16 @@ export interface ImapFlowOptions {
|
|
|
159
165
|
* no further input is parsed.
|
|
160
166
|
*/
|
|
161
167
|
maxResponseSize?: number | undefined;
|
|
168
|
+
/**
|
|
169
|
+
* Maximum number of tokens (atoms, strings, literals, lists and other parse tree nodes) in a
|
|
170
|
+
* single IMAP response. The byte limits bound what a server sends, this bounds what parsing it
|
|
171
|
+
* costs: each token takes around a hundred bytes of heap while the response is parsed, so a
|
|
172
|
+
* response of many small tokens costs about fifty times its size. Defaults to 1000000, far
|
|
173
|
+
* above any legitimate response (a SEARCH answer of plain numbers is not tokenized and does
|
|
174
|
+
* not count). Lower it in a memory-limited runtime. `Infinity` disables the limit. Exceeding it
|
|
175
|
+
* is terminal: the connection fails with error code `TooManyTokens`.
|
|
176
|
+
*/
|
|
177
|
+
maxResponseTokens?: number | undefined;
|
|
162
178
|
/**
|
|
163
179
|
* Threshold in milliseconds for warning that a mailbox lock has been held
|
|
164
180
|
* for a long time (diagnostic for forgotten release() calls). Defaults to
|
|
@@ -681,9 +697,12 @@ export type DownloadManyResult = {
|
|
|
681
697
|
export interface AppendResponseObject {
|
|
682
698
|
/** Full mailbox path where the message was uploaded to */
|
|
683
699
|
destination: string;
|
|
684
|
-
/** Mailbox UIDVALIDITY if server has UIDPLUS extension enabled */
|
|
700
|
+
/** Mailbox UIDVALIDITY if server has UIDPLUS extension enabled, only set together with the `uid` it reported */
|
|
685
701
|
uidValidity?: bigint | undefined;
|
|
686
|
-
/**
|
|
702
|
+
/**
|
|
703
|
+
* UID of the uploaded message if server has UIDPLUS extension enabled. Without a usable APPENDUID,
|
|
704
|
+
* an append to the selected mailbox looks it up by sequence number instead, and `uidValidity` is not set
|
|
705
|
+
*/
|
|
687
706
|
uid?: number | undefined;
|
|
688
707
|
/** Sequence number of the uploaded message if path is currently selected mailbox */
|
|
689
708
|
seq?: number | undefined;
|
|
@@ -775,6 +794,22 @@ export interface ExistsEvent {
|
|
|
775
794
|
/** Message count before this update */
|
|
776
795
|
prevCount: number;
|
|
777
796
|
}
|
|
797
|
+
/**
|
|
798
|
+
* Payload of the `highestModseq` event: the HIGHESTMODSEQ of the selected mailbox changed (RFC
|
|
799
|
+
* 7162), so flags or other message metadata changed on the server. Emitted when a poll on a
|
|
800
|
+
* server without IDLE (`missingIdleCommand` STATUS or SELECT) or a STATUS or re-SELECT of the
|
|
801
|
+
* open mailbox reports a new value, which is when the changes themselves are not delivered as
|
|
802
|
+
* `flags` events. A consumer resynchronizes with `fetch(range, { flags: true }, { changedSince:
|
|
803
|
+
* prevHighestModseq })`.
|
|
804
|
+
*/
|
|
805
|
+
export interface HighestModseqEvent {
|
|
806
|
+
/** Mailbox path */
|
|
807
|
+
path: string;
|
|
808
|
+
/** The new HIGHESTMODSEQ value */
|
|
809
|
+
highestModseq: bigint;
|
|
810
|
+
/** The value before this update, undefined when none was known */
|
|
811
|
+
prevHighestModseq?: bigint | undefined;
|
|
812
|
+
}
|
|
778
813
|
export interface FlagsEvent {
|
|
779
814
|
/** Mailbox path */
|
|
780
815
|
path: string;
|
|
@@ -874,7 +909,10 @@ export interface TlsInfo extends CipherNameAndProtocol {
|
|
|
874
909
|
export interface ImapFlowEvents {
|
|
875
910
|
/** Connection close event */
|
|
876
911
|
close: [];
|
|
877
|
-
/**
|
|
912
|
+
/**
|
|
913
|
+
* Error event. Emitted at most once per connection, the connection closes after it. An error
|
|
914
|
+
* while `connect()` is pending rejects `connect()` instead and is not emitted
|
|
915
|
+
*/
|
|
878
916
|
error: [error: Error];
|
|
879
917
|
/** Message count in currently opened mailbox changed */
|
|
880
918
|
exists: [data: ExistsEvent];
|
|
@@ -882,7 +920,13 @@ export interface ImapFlowEvents {
|
|
|
882
920
|
expunge: [data: ExpungeEvent];
|
|
883
921
|
/** Flags were updated for a message */
|
|
884
922
|
flags: [data: FlagsEvent];
|
|
885
|
-
/**
|
|
923
|
+
/** The HIGHESTMODSEQ of the selected mailbox changed without the changes being reported, see {@link HighestModseqEvent} */
|
|
924
|
+
highestModseq: [data: HighestModseqEvent];
|
|
925
|
+
/**
|
|
926
|
+
* Mailbox was opened. Also emitted, after `mailboxClose` for the old mailbox object, when the
|
|
927
|
+
* open mailbox turned out to have a new UIDVALIDITY (a re-SELECT, or a STATUS poll on a server
|
|
928
|
+
* without IDLE, which then selects the mailbox again): every cached UID of it is invalid
|
|
929
|
+
*/
|
|
886
930
|
mailboxOpen: [mailbox: MailboxObject];
|
|
887
931
|
/** Mailbox was closed, either explicitly or because the connection went away while a mailbox was still selected */
|
|
888
932
|
mailboxClose: [mailbox: MailboxObject];
|
|
@@ -141,11 +141,14 @@ export default async function append(connection, destination, content, flags, id
|
|
|
141
141
|
// message was already stored and a retrying caller would duplicate it.
|
|
142
142
|
let uidValidity = parseBigIntValue(section[1] && section[1].value, MAX_UINT32_DIGITS);
|
|
143
143
|
let uid = parseUintValue(section[2] && section[2].value, MAX_UINT32_DIGITS);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
// UIDVALIDITY is only reported next to the UID it was sent with: kept without one,
|
|
145
|
+
// it would end up paired with the UID the SEARCH fallback below guesses from the
|
|
146
|
+
// sequence number, and the pair would pass for a server-confirmed identity.
|
|
147
147
|
if (uid) {
|
|
148
148
|
map.uid = uid;
|
|
149
|
+
if (uidValidity) {
|
|
150
|
+
map.uidValidity = uidValidity;
|
|
151
|
+
}
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
154
|
}
|
|
@@ -25,6 +25,7 @@ export interface FetchCommandResult {
|
|
|
25
25
|
* @param range - Message sequence number or UID range
|
|
26
26
|
* @param query - Fetch query specifying which data to retrieve (e.g., flags, envelope, bodyStructure, headers, source, bodyParts)
|
|
27
27
|
* @param options - Fetch options
|
|
28
|
-
* @returns Object with message count and list, or undefined
|
|
28
|
+
* @returns Object with message count and list, or undefined when the range is empty
|
|
29
|
+
* @throws NoMailboxSelected when not in SELECTED state (NoConnection once the connection is gone)
|
|
29
30
|
*/
|
|
30
31
|
export default function fetch(connection: ImapFlow, range: string, query: FetchQueryObject, options?: FetchCommandOptions | undefined): Promise<FetchCommandResult | undefined>;
|
|
@@ -7,11 +7,17 @@ import { createImapError } from '../errors.js';
|
|
|
7
7
|
* @param range - Message sequence number or UID range
|
|
8
8
|
* @param query - Fetch query specifying which data to retrieve (e.g., flags, envelope, bodyStructure, headers, source, bodyParts)
|
|
9
9
|
* @param options - Fetch options
|
|
10
|
-
* @returns Object with message count and list, or undefined
|
|
10
|
+
* @returns Object with message count and list, or undefined when the range is empty
|
|
11
|
+
* @throws NoMailboxSelected when not in SELECTED state (NoConnection once the connection is gone)
|
|
11
12
|
*/
|
|
12
13
|
export default async function fetch(connection, range, query, options) {
|
|
13
14
|
let mailbox = getSelectedMailbox(connection);
|
|
14
|
-
if (!mailbox
|
|
15
|
+
if (!mailbox) {
|
|
16
|
+
// queued behind a CLOSE or a failed SELECT: rejecting keeps fetch() from yielding
|
|
17
|
+
// nothing as if the mailbox were empty
|
|
18
|
+
throw connection.createNoMailboxError('FETCH');
|
|
19
|
+
}
|
|
20
|
+
if (!range) {
|
|
15
21
|
// nothing to do here
|
|
16
22
|
return;
|
|
17
23
|
}
|
|
@@ -212,6 +212,7 @@ async function pollOnce(connection, session) {
|
|
|
212
212
|
// HIGHESTMODSEQ is filtered out again unless the server advertises CONDSTORE, so a
|
|
213
213
|
// CONDSTORE session keeps mailbox.highestModseq current without asking a plain
|
|
214
214
|
// server for an item it does not know.
|
|
215
|
+
let polled = connection.mailbox;
|
|
215
216
|
let status = await connection.runInternal('STATUS', path, {
|
|
216
217
|
messages: true,
|
|
217
218
|
uidNext: true,
|
|
@@ -222,6 +223,12 @@ async function pollOnce(connection, session) {
|
|
|
222
223
|
if (!status) {
|
|
223
224
|
throw createImapError('STATUS poll failed', 'PollFailed');
|
|
224
225
|
}
|
|
226
|
+
// STATUS can only tell that the UIDs changed, the selected view is stale then: select
|
|
227
|
+
// the mailbox again, which reports it as closed and opened anew with the new state
|
|
228
|
+
if (polled && connection.mailbox === polled && status.uidValidity && polled.uidValidity && status.uidValidity !== polled.uidValidity) {
|
|
229
|
+
connection.log.info({ msg: 'UIDVALIDITY changed, selecting the mailbox again', path, cid: connection.id });
|
|
230
|
+
await connection.runInternal('SELECT', path, { readOnly: session.selectCommand.command === 'EXAMINE' });
|
|
231
|
+
}
|
|
225
232
|
break;
|
|
226
233
|
}
|
|
227
234
|
case 'NOOP':
|
|
@@ -211,8 +211,13 @@ export default async function select(connection, pathInput, options) {
|
|
|
211
211
|
let currentMailbox = connection.mailbox;
|
|
212
212
|
connection.mailbox = false;
|
|
213
213
|
// Emit mailboxClose if we're switching from a different mailbox.
|
|
214
|
-
// Re-selecting the same mailbox (e.g., for resync) does not trigger close/open
|
|
215
|
-
|
|
214
|
+
// Re-selecting the same mailbox (e.g., for resync) does not trigger close/open, unless
|
|
215
|
+
// it came back with a new UIDVALIDITY: the old view and every UID in it are gone then,
|
|
216
|
+
// which is reported the way a fresh mailboxOpen() of another mailbox would be.
|
|
217
|
+
let reopened = !currentMailbox ||
|
|
218
|
+
currentMailbox.path !== path ||
|
|
219
|
+
(!!currentMailbox.uidValidity && !!map.uidValidity && currentMailbox.uidValidity !== map.uidValidity);
|
|
220
|
+
if (currentMailbox && reopened) {
|
|
216
221
|
emitSafe(connection, 'mailboxClose', currentMailbox);
|
|
217
222
|
}
|
|
218
223
|
connection.mailbox = map;
|
|
@@ -220,13 +225,19 @@ export default async function select(connection, pathInput, options) {
|
|
|
220
225
|
// re-issues the SELECT to detect changes on servers without IDLE support).
|
|
221
226
|
connection.currentSelectCommand = selectCommand;
|
|
222
227
|
connection.state = connection.states.SELECTED;
|
|
223
|
-
if (
|
|
228
|
+
if (reopened || !currentMailbox) {
|
|
224
229
|
emitSafe(connection, 'mailboxOpen', connection.mailbox);
|
|
225
230
|
}
|
|
226
|
-
else
|
|
231
|
+
else {
|
|
227
232
|
// A re-SELECT of the open mailbox (the SELECT polling fallback) gets its EXISTS here
|
|
228
|
-
// instead of in the global handler, so it is reported the same way
|
|
229
|
-
|
|
233
|
+
// instead of in the global handler, so it is reported the same way. Flag changes are
|
|
234
|
+
// not reported by SELECT, a new HIGHESTMODSEQ is the cue to resync them.
|
|
235
|
+
if (typeof map.exists === 'number' && map.exists !== currentMailbox.exists) {
|
|
236
|
+
emitSafe(connection, 'exists', { path, count: map.exists, prevCount: currentMailbox.exists });
|
|
237
|
+
}
|
|
238
|
+
if (map.highestModseq && map.highestModseq !== currentMailbox.highestModseq) {
|
|
239
|
+
emitSafe(connection, 'highestModseq', { path, highestModseq: map.highestModseq, prevHighestModseq: currentMailbox.highestModseq });
|
|
240
|
+
}
|
|
230
241
|
}
|
|
231
242
|
response.next();
|
|
232
243
|
return map;
|
|
@@ -14,8 +14,13 @@ const MAILBOX_UPDATERS = {
|
|
|
14
14
|
uidNext: (value, mailbox) => {
|
|
15
15
|
mailbox.uidNext = value;
|
|
16
16
|
},
|
|
17
|
-
highestModseq: (value, mailbox) => {
|
|
18
|
-
mailbox.highestModseq
|
|
17
|
+
highestModseq: (value, mailbox, connection, path) => {
|
|
18
|
+
let prevHighestModseq = mailbox.highestModseq;
|
|
19
|
+
if (prevHighestModseq !== value) {
|
|
20
|
+
mailbox.highestModseq = value;
|
|
21
|
+
// the changes behind it are not reported by STATUS, the event is the cue to resync
|
|
22
|
+
emitSafe(connection, 'highestModseq', { path, highestModseq: value, prevHighestModseq });
|
|
23
|
+
}
|
|
19
24
|
}
|
|
20
25
|
};
|
|
21
26
|
/**
|
|
@@ -65,13 +70,22 @@ export default async function status(connection, path, query) {
|
|
|
65
70
|
if (!list) {
|
|
66
71
|
return;
|
|
67
72
|
}
|
|
73
|
+
let updates = [];
|
|
68
74
|
parseStatusList(list, (key, value) => {
|
|
69
75
|
map[key] = value;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
updater(value, currentMailbox, connection, path);
|
|
76
|
+
if (currentMailbox && MAILBOX_UPDATERS[key]) {
|
|
77
|
+
updates.push([key, value]);
|
|
73
78
|
}
|
|
74
79
|
});
|
|
80
|
+
// Under a new UIDVALIDITY the counts describe a mailbox the selected view no
|
|
81
|
+
// longer matches, so they are not applied to it; the STATUS poll selects the
|
|
82
|
+
// mailbox again instead, which reports it as opened anew
|
|
83
|
+
if (currentMailbox && map.uidValidity && currentMailbox.uidValidity && map.uidValidity !== currentMailbox.uidValidity) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (let [key, value] of updates) {
|
|
87
|
+
MAILBOX_UPDATERS[key](value, currentMailbox, connection, path);
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
90
|
}
|
|
77
91
|
});
|