imapflow 1.3.6 → 1.3.7

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.
@@ -0,0 +1,609 @@
1
+ 'use strict';
2
+
3
+ // Targeted coverage for ImapFlow internals that need precise state setups:
4
+ // the lock queue processor (processLocks), authentication fallbacks, and a few
5
+ // reader/handler error branches.
6
+
7
+ const { ImapFlow } = require('../lib/imap-flow');
8
+
9
+ const makeClient = (overrides = {}) => {
10
+ let client = new ImapFlow({
11
+ host: 'imap.example.com',
12
+ port: 993,
13
+ auth: { user: 'test', pass: 'test' },
14
+ logger: false,
15
+ ...overrides
16
+ });
17
+ client.socket = { destroyed: false, destroy: () => {} };
18
+ client.usable = true;
19
+ return client;
20
+ };
21
+
22
+ const drain = () => new Promise(resolve => setImmediate(resolve));
23
+
24
+ // ============================================================================
25
+ // processLocks
26
+ // ============================================================================
27
+
28
+ module.exports['Coverage: processLocks is re-entrant safe (already processing)'] = async test => {
29
+ let client = makeClient();
30
+ let resolveOpen;
31
+ // mailboxOpen blocks until we resolve it, keeping processingLock busy
32
+ client.mailboxOpen = () =>
33
+ new Promise(resolve => {
34
+ resolveOpen = () => resolve({ path: 'A' });
35
+ });
36
+
37
+ let lockAPromise = client.getMailboxLock('A');
38
+ await drain(); // let processLocks start and await mailboxOpen
39
+
40
+ // Second request while the first is still being processed -> hits the
41
+ // "already processing" early return.
42
+ let lockBPromise = client.getMailboxLock('B');
43
+ await drain();
44
+
45
+ // Now let the first mailbox open complete
46
+ client.mailbox = { path: 'A', readOnly: false };
47
+ resolveOpen();
48
+ let lockA = await lockAPromise;
49
+ test.ok(lockA);
50
+
51
+ // Releasing A lets B be processed; B opens a different mailbox
52
+ client.mailboxOpen = async () => {
53
+ client.mailbox = { path: 'B', readOnly: false };
54
+ return { path: 'B' };
55
+ };
56
+ lockA.release();
57
+ let lockB = await lockBPromise;
58
+ test.ok(lockB);
59
+ lockB.release();
60
+ await drain();
61
+ test.done();
62
+ };
63
+
64
+ module.exports['Coverage: processLocks yields to the event loop on many failing locks'] = async test => {
65
+ let client = makeClient();
66
+ client.usable = false; // every lock fails immediately via the NoConnection path
67
+
68
+ let promises = [];
69
+ for (let i = 0; i < 7; i++) {
70
+ promises.push(client.getMailboxLock('M' + i).catch(err => err));
71
+ }
72
+ let results = await Promise.all(promises);
73
+ test.equal(results.length, 7);
74
+ results.forEach(r => test.equal(r.code, 'NoConnection'));
75
+ test.done();
76
+ };
77
+
78
+ module.exports['Coverage: processLocks marks mailbox missing when SELECT NO and LIST verify throws'] = async test => {
79
+ let client = makeClient();
80
+ client.mailboxOpen = async () => {
81
+ let err = new Error('SELECT failed');
82
+ err.responseStatus = 'NO';
83
+ throw err;
84
+ };
85
+ // run('LIST') used to verify existence throws -> inner catch (3849-3850)
86
+ client.run = async () => {
87
+ throw new Error('LIST blew up');
88
+ };
89
+ let err = null;
90
+ try {
91
+ await client.getMailboxLock('Ghost');
92
+ } catch (e) {
93
+ err = e;
94
+ }
95
+ test.ok(err);
96
+ test.equal(err.responseStatus, 'NO');
97
+ test.done();
98
+ };
99
+
100
+ module.exports['Coverage: processLocks marks mailboxMissing when LIST returns empty'] = async test => {
101
+ let client = makeClient();
102
+ client.mailboxOpen = async () => {
103
+ let err = new Error('SELECT failed');
104
+ err.responseStatus = 'NO';
105
+ throw err;
106
+ };
107
+ client.run = async () => []; // empty LIST -> mailbox confirmed missing
108
+ let err = null;
109
+ try {
110
+ await client.getMailboxLock('Ghost');
111
+ } catch (e) {
112
+ err = e;
113
+ }
114
+ test.ok(err.mailboxMissing);
115
+ test.done();
116
+ };
117
+
118
+ module.exports['Coverage: processLocks yields after 5 processed locks'] = async test => {
119
+ let client = makeClient();
120
+ client.usable = false; // each queued lock fails and continues
121
+ let rejects = 0;
122
+ for (let i = 0; i < 6; i++) {
123
+ client.locks.push({
124
+ resolve: () => {},
125
+ reject: () => {
126
+ rejects++;
127
+ },
128
+ path: 'P',
129
+ options: {},
130
+ lockId: i
131
+ });
132
+ }
133
+ await client.processLocks();
134
+ test.equal(rejects, 6);
135
+ test.done();
136
+ };
137
+
138
+ module.exports['Coverage: getMailboxLock includes description in trace logs'] = async test => {
139
+ let client = makeClient();
140
+ client.mailbox = { path: 'INBOX', readOnly: false };
141
+ // a description on the lock options exercises the `...(options.description && {...})` spreads
142
+ let lock = await client.getMailboxLock('INBOX', { description: 'my work' });
143
+ test.ok(lock);
144
+ lock.release();
145
+ await drain();
146
+ test.done();
147
+ };
148
+
149
+ module.exports['Coverage: processLocks logs active lock and returns when already processing'] = async test => {
150
+ let client = makeClient();
151
+ client.mailbox = { path: 'INBOX' }; // so the log records the active mailbox path
152
+ client.processingLock = true; // simulate a concurrent processor
153
+ client.currentLock = { lockId: 7, options: { description: 'held lock' } };
154
+ // returns immediately after logging the active lock details
155
+ await client.processLocks();
156
+ test.ok(true);
157
+ test.done();
158
+ };
159
+
160
+ // ============================================================================
161
+ // close() internal error handling
162
+ // ============================================================================
163
+
164
+ module.exports['Coverage: close swallows streamer cleanup errors'] = test => {
165
+ let client = makeClient();
166
+ client.streamer = {
167
+ removeListener() {},
168
+ destroyed: false,
169
+ destroy() {
170
+ throw new Error('streamer destroy boom');
171
+ }
172
+ };
173
+ // Should not throw despite the streamer.destroy failure
174
+ client.close();
175
+ test.ok(true);
176
+ test.done();
177
+ };
178
+
179
+ module.exports['Coverage: close swallows top-level errors'] = test => {
180
+ let client = makeClient();
181
+ // folders.clear() throws -> the outer try/catch in close() handles it
182
+ client.folders = {
183
+ clear() {
184
+ throw new Error('folders boom');
185
+ }
186
+ };
187
+ client.close();
188
+ test.ok(true);
189
+ test.done();
190
+ };
191
+
192
+ module.exports['Coverage: lock acquired via SELECT logs description'] = async test => {
193
+ let client = makeClient();
194
+ client.mailbox = false; // force the SELECT/EXAMINE path
195
+ client.mailboxOpen = async path => {
196
+ client.mailbox = { path, readOnly: false };
197
+ return { path };
198
+ };
199
+ let lock = await client.getMailboxLock('Work', { description: 'selected-path lock' });
200
+ test.equal(client.mailbox.path, 'Work');
201
+ lock.release();
202
+ await drain();
203
+ test.done();
204
+ };
205
+
206
+ module.exports['Coverage: lock failure logs description'] = async test => {
207
+ let client = makeClient();
208
+ client.mailbox = false;
209
+ client.mailboxOpen = async () => {
210
+ throw new Error('cannot open');
211
+ };
212
+ let err = null;
213
+ try {
214
+ await client.getMailboxLock('Bad', { description: 'failing lock' });
215
+ } catch (e) {
216
+ err = e;
217
+ }
218
+ test.ok(err);
219
+ test.done();
220
+ };
221
+
222
+ module.exports['Coverage: getMailboxLock logs active lock description while one is held'] = test => {
223
+ let client = makeClient();
224
+ // a held lock carrying a description exercises the activeLock spread in the request log
225
+ client.currentLock = { lockId: 1, options: { description: 'currently held' } };
226
+ let p = client.getMailboxLock('Queued', { description: 'queued lock' });
227
+ p.catch(() => {}); // it will never resolve (held lock blocks); just exercise the log
228
+ test.ok(p && typeof p.then === 'function');
229
+ client.close(); // rejects the queued lock and cleans up
230
+ test.done();
231
+ };
232
+
233
+ // ============================================================================
234
+ // initialOK / serverBye fallbacks
235
+ // ============================================================================
236
+
237
+ module.exports['Coverage: initialOK tolerates a greeting with no attributes'] = async test => {
238
+ let client = makeClient();
239
+ client.isClosed = true; // so beginSession short-circuits without starting a session
240
+ await client.initialOK({}); // no .attributes -> (message.attributes || [])
241
+ test.equal(client.greeting, '');
242
+ test.done();
243
+ };
244
+
245
+ module.exports['Coverage: serverBye defaults the reason when none is given'] = async test => {
246
+ let client = makeClient();
247
+ await client.serverBye({}); // no attributes -> reason falls back to the default
248
+ test.equal(client.byeReason, 'Server closed connection');
249
+ test.done();
250
+ };
251
+
252
+ // ============================================================================
253
+ // authenticate fallbacks
254
+ // ============================================================================
255
+
256
+ module.exports['Coverage: authenticate throws when state is LOGOUT'] = async test => {
257
+ let client = makeClient();
258
+ client.state = client.states.LOGOUT;
259
+ let err = null;
260
+ try {
261
+ await client.authenticate();
262
+ } catch (e) {
263
+ err = e;
264
+ }
265
+ test.ok(err);
266
+ test.done();
267
+ };
268
+
269
+ module.exports['Coverage: authenticate returns true when already authenticated'] = async test => {
270
+ let client = makeClient();
271
+ client.state = client.states.AUTHENTICATED;
272
+ let res = await client.authenticate();
273
+ test.equal(res, true);
274
+ test.done();
275
+ };
276
+
277
+ module.exports['Coverage: authenticate throws when run yields falsy auth result'] = async test => {
278
+ let client = makeClient();
279
+ client.state = client.states.NOT_AUTHENTICATED;
280
+ client.capabilities = new Map(); // no AUTH= -> LOGIN path
281
+ client.run = async () => false; // LOGIN returns falsy -> "No matching authentication method"
282
+ let err = null;
283
+ try {
284
+ await client.authenticate();
285
+ } catch (e) {
286
+ err = e;
287
+ }
288
+ test.ok(err);
289
+ test.done();
290
+ };
291
+
292
+ // ============================================================================
293
+ // Socket event handlers (built by setSocketHandlers)
294
+ // ============================================================================
295
+
296
+ // Minimal socket stub that records listeners so handlers can be invoked directly.
297
+ const makeSocketStub = () => ({
298
+ destroyed: false,
299
+ once() {},
300
+ on() {},
301
+ removeListener() {},
302
+ destroy() {}
303
+ });
304
+
305
+ module.exports['Coverage: setSocketHandlers removes a lingering connect error handler'] = test => {
306
+ let client = makeClient();
307
+ let removed = null;
308
+ client.socket = {
309
+ destroyed: false,
310
+ once() {},
311
+ on() {},
312
+ removeListener(evt, fn) {
313
+ removed = fn;
314
+ },
315
+ destroy() {}
316
+ };
317
+ client.writeSocket = client.socket;
318
+ let handler = () => {};
319
+ client._connectErrorHandler = handler;
320
+ client.setSocketHandlers();
321
+ test.equal(removed, handler, 'temporary connect error handler removed');
322
+ test.equal(client._connectErrorHandler, null);
323
+ test.done();
324
+ };
325
+
326
+ module.exports['Coverage: initialPREAUTH is a no-op once closed'] = async test => {
327
+ let client = makeClient();
328
+ client.isClosed = true;
329
+ let started = false;
330
+ client.startSession = async () => {
331
+ started = true;
332
+ };
333
+ await client.initialPREAUTH();
334
+ test.equal(started, false);
335
+ test.done();
336
+ };
337
+
338
+ module.exports['Coverage: initialPREAUTH routes startSession failure to onUnhandledError'] = async test => {
339
+ let client = makeClient();
340
+ client.initialResolve = false;
341
+ client.initialReject = false; // no pending connect promise -> fall through to onUnhandledError
342
+ client.startSession = async () => {
343
+ throw new Error('session boom');
344
+ };
345
+ let closedAfter = false;
346
+ client.closeAfter = () => {
347
+ closedAfter = true;
348
+ };
349
+ await client.initialPREAUTH();
350
+ await drain();
351
+ await drain();
352
+ test.equal(client.state, client.states.AUTHENTICATED);
353
+ test.ok(closedAfter, 'closeAfter invoked from the PREAUTH error handler');
354
+ test.done();
355
+ };
356
+
357
+ module.exports['Coverage: _socketError logs and emits the error'] = test => {
358
+ let client = makeClient();
359
+ client.socket = makeSocketStub();
360
+ client.writeSocket = client.socket;
361
+ client.setSocketHandlers();
362
+ let emitted = null;
363
+ client.on('error', err => {
364
+ emitted = err;
365
+ });
366
+ // not connecting/upgrading -> emitError falls through to emit('error')
367
+ client._socketError(new Error('socket failure'));
368
+ test.ok(emitted);
369
+ test.done();
370
+ };
371
+
372
+ module.exports['Coverage: _socketTimeout recovers an IDLE connection with NOOP'] = async test => {
373
+ let client = makeClient();
374
+ client.socket = makeSocketStub();
375
+ client.writeSocket = client.socket;
376
+ client.setSocketHandlers();
377
+ client.idling = true;
378
+ client.usable = true;
379
+ let noopRun = false;
380
+ let idleResumed = false;
381
+ client.run = async cmd => {
382
+ if (cmd === 'NOOP') noopRun = true;
383
+ return true;
384
+ };
385
+ client.idle = async () => {
386
+ idleResumed = true;
387
+ };
388
+ client._socketTimeout();
389
+ await drain();
390
+ await drain();
391
+ test.ok(noopRun, 'NOOP issued to recover IDLE');
392
+ test.ok(idleResumed, 'IDLE resumed after NOOP');
393
+ test.done();
394
+ };
395
+
396
+ module.exports['Coverage: _socketTimeout emits error when idling but unusable'] = test => {
397
+ let client = makeClient();
398
+ client.socket = makeSocketStub();
399
+ client.writeSocket = client.socket;
400
+ client.setSocketHandlers();
401
+ client.idling = true;
402
+ client.usable = false; // cannot recover -> emitError
403
+ let emitted = null;
404
+ client.on('error', err => {
405
+ emitted = err;
406
+ });
407
+ client._socketTimeout();
408
+ test.ok(emitted);
409
+ test.equal(emitted.code, 'ETIMEOUT');
410
+ test.done();
411
+ };
412
+
413
+ module.exports['Coverage: _socketTimeout emits error for non-IDLE operations'] = test => {
414
+ let client = makeClient();
415
+ client.socket = makeSocketStub();
416
+ client.writeSocket = client.socket;
417
+ client.setSocketHandlers();
418
+ client.idling = false;
419
+ let emitted = null;
420
+ client.on('error', err => {
421
+ emitted = err;
422
+ });
423
+ client._socketTimeout();
424
+ test.ok(emitted);
425
+ test.equal(emitted.code, 'ETIMEOUT');
426
+ test.done();
427
+ };
428
+
429
+ module.exports['Coverage: _socketTimeout IDLE recovery closes on failure'] = async test => {
430
+ let client = makeClient();
431
+ client.socket = makeSocketStub();
432
+ client.writeSocket = client.socket;
433
+ client.setSocketHandlers();
434
+ client.idling = true;
435
+ client.usable = true;
436
+ client.run = async () => {
437
+ throw new Error('NOOP failed');
438
+ };
439
+ let closed = false;
440
+ client.close = () => {
441
+ closed = true;
442
+ };
443
+ client._socketTimeout();
444
+ await drain();
445
+ await drain();
446
+ test.ok(closed, 'connection closed after failed IDLE recovery');
447
+ test.done();
448
+ };
449
+
450
+ // ============================================================================
451
+ // reader() periodic event-loop yield
452
+ // ============================================================================
453
+
454
+ module.exports['Coverage: reader yields after processing many items in one pass'] = async test => {
455
+ let client = makeClient();
456
+ client.mailbox = { path: 'INBOX', exists: 0 };
457
+ // Queue 12 untagged EXISTS responses so a single reader() pass processes >10
458
+ // items and hits the periodic `await setImmediate` yield.
459
+ let items = [];
460
+ for (let i = 1; i <= 12; i++) {
461
+ items.push({ payload: Buffer.from(`* ${i} EXISTS`), literals: [], next: () => {} });
462
+ }
463
+ let idx = 0;
464
+ client.streamer = {
465
+ read: () => (idx < items.length ? items[idx++] : null)
466
+ };
467
+ client.on('exists', () => {});
468
+ await client.reader();
469
+ test.equal(idx, 12, 'all items drained in one reader pass');
470
+ test.done();
471
+ };
472
+
473
+ // ============================================================================
474
+ // reader() error branches (handlers throwing must not break the loop)
475
+ // ============================================================================
476
+
477
+ // Helper: drive reader() over a fixed list of pre-built data items.
478
+ const runReaderWith = async (client, payloads) => {
479
+ let items = payloads.map(p => ({ payload: Buffer.from(p), literals: [], next: () => {} }));
480
+ let idx = 0;
481
+ client.streamer = { read: () => (idx < items.length ? items[idx++] : null) };
482
+ await client.reader();
483
+ return idx;
484
+ };
485
+
486
+ module.exports['Coverage: reader swallows a throwing onPlusTag handler'] = async test => {
487
+ let client = makeClient();
488
+ client.currentRequest = {
489
+ options: {
490
+ onPlusTag: async () => {
491
+ throw new Error('plus boom');
492
+ }
493
+ }
494
+ };
495
+ let count = await runReaderWith(client, ['+ go ahead']);
496
+ test.equal(count, 1);
497
+ test.done();
498
+ };
499
+
500
+ module.exports['Coverage: reader swallows a throwing section handler'] = async test => {
501
+ let client = makeClient();
502
+ client.sectionHandlers = {
503
+ CAPABILITY: async () => {
504
+ throw new Error('section boom');
505
+ }
506
+ };
507
+ let count = await runReaderWith(client, ['* OK [CAPABILITY FOO] hi']);
508
+ test.equal(count, 1);
509
+ test.done();
510
+ };
511
+
512
+ module.exports['Coverage: reader swallows a throwing untagged handler'] = async test => {
513
+ let client = makeClient();
514
+ client.untaggedHandlers = {
515
+ FOO: async () => {
516
+ throw new Error('untagged boom');
517
+ }
518
+ };
519
+ let count = await runReaderWith(client, ['* FOO bar']);
520
+ test.equal(count, 1);
521
+ test.done();
522
+ };
523
+
524
+ module.exports['Coverage: reader swallows a throwing trySend after a tagged response'] = async test => {
525
+ let client = makeClient();
526
+ let resolved = false;
527
+ client.currentRequest = { tag: 'A1' };
528
+ client.requestTagMap.set('A1', {
529
+ command: 'NOOP',
530
+ attributes: [],
531
+ options: {},
532
+ resolve: arg => {
533
+ resolved = true;
534
+ // the reader awaits arg.next(); call it so the loop proceeds
535
+ arg.next();
536
+ },
537
+ reject: () => {}
538
+ });
539
+ client.trySend = async () => {
540
+ throw new Error('trySend boom');
541
+ };
542
+ let count = await runReaderWith(client, ['A1 OK done']);
543
+ test.equal(count, 1);
544
+ test.ok(resolved, 'request still resolved despite trySend failure');
545
+ test.done();
546
+ };
547
+
548
+ module.exports['Coverage: reader handles compiler failure when building executedCommand'] = async test => {
549
+ let client = makeClient();
550
+ let rejected = null;
551
+ // A request whose attributes cannot be compiled -> the executedCommand try/catch is hit
552
+ client.currentRequest = { tag: 'A1' };
553
+ client.requestTagMap.set('A1', {
554
+ command: 'NOOP',
555
+ // a circular/odd attribute that makes the logging compiler throw
556
+ attributes: [{ type: 'LITERAL', get value() { throw new Error('compile boom'); } }],
557
+ options: {},
558
+ resolve: () => {},
559
+ reject: err => {
560
+ rejected = err;
561
+ }
562
+ });
563
+ await runReaderWith(client, ['A1 NO it failed']);
564
+ test.ok(rejected);
565
+ test.equal(rejected.responseStatus, 'NO');
566
+ test.done();
567
+ };
568
+
569
+ module.exports['Coverage: reader caps very large throttle backoff'] = async test => {
570
+ let client = makeClient();
571
+ let rejected = null;
572
+ client.currentRequest = { tag: 'A1' };
573
+ client.requestTagMap.set('A1', {
574
+ command: 'FETCH',
575
+ attributes: [],
576
+ options: {},
577
+ resolve: () => {},
578
+ reject: err => {
579
+ rejected = err;
580
+ }
581
+ });
582
+ // Run the reader; immediately abort the throttle wait via close() so the test
583
+ // does not actually wait. The large backoff value exercises the 5-minute cap.
584
+ let readerPromise = runReaderWith(client, ['A1 BAD Request is throttled. Suggested Backoff Time: 999999999 milliseconds']);
585
+ // give the reader a tick to register the throttle timer, then abort it
586
+ await drain();
587
+ if (typeof client._throttleAbort === 'function') {
588
+ client._throttleAbort(true);
589
+ }
590
+ await readerPromise;
591
+ test.ok(rejected);
592
+ test.done();
593
+ };
594
+
595
+ // ============================================================================
596
+ // beginSession when already closed
597
+ // ============================================================================
598
+
599
+ module.exports['Coverage: beginSession is a no-op once closed'] = test => {
600
+ let client = makeClient();
601
+ client.isClosed = true;
602
+ let started = false;
603
+ client.startSession = async () => {
604
+ started = true;
605
+ };
606
+ client.beginSession(() => {});
607
+ test.equal(started, false, 'startSession not invoked when closed');
608
+ test.done();
609
+ };