imapflow 1.6.6 → 1.7.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +20 -0
- package/README.md +2 -3
- package/lib/commands/authenticate.js +15 -5
- package/lib/commands/idle.js +46 -18
- package/lib/imap-flow.d.ts +15 -1
- package/lib/imap-flow.js +373 -92
- package/lib/proxy-connection.js +14 -4
- package/lib/tools.js +29 -0
- package/package.json +3 -3
- package/test/auto-idle-test.js +470 -0
- package/test/connection-edge-cases-test.js +3 -1
- package/test/fixtures/test-client.js +57 -0
- package/test/idle-polling-test.js +88 -0
- package/test/imap-flow-coverage-test.js +4 -10
- package/test/imap-flow-fetch-download-test.js +3 -10
- package/test/imap-flow-internals-test.js +168 -50
- package/test/timer-policy-test.js +4 -17
|
@@ -428,3 +428,91 @@ module.exports['Polling: a break in the initiation tick prevents the first poll'
|
|
|
428
428
|
test.done();
|
|
429
429
|
});
|
|
430
430
|
};
|
|
431
|
+
|
|
432
|
+
module.exports['Polling: a restarted session resumes the schedule instead of polling again'] = async test => {
|
|
433
|
+
await withFakeTimers(async timers => {
|
|
434
|
+
let connection = createConnection();
|
|
435
|
+
|
|
436
|
+
let first = idleCommand(connection, 60000);
|
|
437
|
+
await timers.drain();
|
|
438
|
+
test.deepEqual(connection.commands, ['NOOP'], 'the first session polls immediately');
|
|
439
|
+
|
|
440
|
+
await connection.preCheck();
|
|
441
|
+
await first;
|
|
442
|
+
|
|
443
|
+
// Auto-IDLE restarts the loop after every caller command. An unconditional first poll here
|
|
444
|
+
// would tie the poll rate to how often the caller runs commands instead of to the poll
|
|
445
|
+
// interval - with a short autoIdleDelay, a command every few seconds means a poll every
|
|
446
|
+
// few seconds.
|
|
447
|
+
let second = idleCommand(connection, 60000);
|
|
448
|
+
await timers.drain();
|
|
449
|
+
test.deepEqual(connection.commands, ['NOOP'], 'the restarted session does not poll again');
|
|
450
|
+
test.equal(timers.count(), 1, 'it waits out the remainder of the interval instead');
|
|
451
|
+
test.ok(timers.pending()[0].delay <= 60000, 'and never longer than a full interval');
|
|
452
|
+
|
|
453
|
+
await connection.preCheck();
|
|
454
|
+
await second;
|
|
455
|
+
|
|
456
|
+
// Once a full interval has elapsed, a fresh session polls at once again
|
|
457
|
+
connection._lastPollAt = Date.now() - 61000;
|
|
458
|
+
let third = idleCommand(connection, 60000);
|
|
459
|
+
await timers.drain();
|
|
460
|
+
test.deepEqual(connection.commands, ['NOOP', 'NOOP'], 'a session starting after the interval polls right away');
|
|
461
|
+
|
|
462
|
+
await connection.preCheck();
|
|
463
|
+
await third;
|
|
464
|
+
test.done();
|
|
465
|
+
});
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
module.exports['Polling: a failed poll does not defer the next session'] = async test => {
|
|
469
|
+
await withFakeTimers(async timers => {
|
|
470
|
+
let failNext = true;
|
|
471
|
+
let connection = createConnection({
|
|
472
|
+
exec: async command => {
|
|
473
|
+
connection.commands.push(command);
|
|
474
|
+
if (failNext) {
|
|
475
|
+
failNext = false;
|
|
476
|
+
throw new Error('poll failed');
|
|
477
|
+
}
|
|
478
|
+
return { next: () => {} };
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// The failing poll cancels its own session. The attempt checked nothing, so it must not
|
|
483
|
+
// count as a poll: only a completed poll moves the schedule stamp forward.
|
|
484
|
+
let first = idleCommand(connection, 60000);
|
|
485
|
+
await timers.drain();
|
|
486
|
+
await first;
|
|
487
|
+
test.deepEqual(connection.commands, ['NOOP'], 'the first poll ran and failed');
|
|
488
|
+
|
|
489
|
+
let second = idleCommand(connection, 60000);
|
|
490
|
+
await timers.drain();
|
|
491
|
+
test.deepEqual(connection.commands, ['NOOP', 'NOOP'], 'the next session retries immediately instead of waiting out an interval');
|
|
492
|
+
|
|
493
|
+
await connection.preCheck();
|
|
494
|
+
await second;
|
|
495
|
+
test.done();
|
|
496
|
+
});
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
module.exports['Polling: a backward clock step never defers the next poll past one interval'] = async test => {
|
|
500
|
+
await withFakeTimers(async timers => {
|
|
501
|
+
let connection = createConnection();
|
|
502
|
+
|
|
503
|
+
// A last-poll stamp in the future is what an NTP step or a VM clock sync leaves behind.
|
|
504
|
+
// Without the clamp the remainder math would schedule the next poll a full clock jump
|
|
505
|
+
// plus one interval away.
|
|
506
|
+
connection._lastPollAt = Date.now() + 60 * 60 * 1000;
|
|
507
|
+
|
|
508
|
+
let idlePromise = idleCommand(connection, 60000);
|
|
509
|
+
await timers.drain();
|
|
510
|
+
test.deepEqual(connection.commands, [], 'no immediate poll - the schedule is resumed');
|
|
511
|
+
test.equal(timers.count(), 1, 'a poll timer is armed');
|
|
512
|
+
test.ok(timers.pending()[0].delay <= 60000, 'and it is never more than one interval away');
|
|
513
|
+
|
|
514
|
+
await connection.preCheck();
|
|
515
|
+
await idlePromise;
|
|
516
|
+
test.done();
|
|
517
|
+
});
|
|
518
|
+
};
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// reader/handler error branches.
|
|
6
6
|
|
|
7
7
|
const { ImapFlow } = require('../lib/imap-flow');
|
|
8
|
+
const { makeSocketStub } = require('./fixtures/test-client');
|
|
8
9
|
|
|
9
10
|
const makeClient = (overrides = {}) => {
|
|
10
11
|
let client = new ImapFlow({
|
|
@@ -293,15 +294,6 @@ module.exports['Coverage: authenticate throws when run yields falsy auth result'
|
|
|
293
294
|
// Socket event handlers (built by setSocketHandlers)
|
|
294
295
|
// ============================================================================
|
|
295
296
|
|
|
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
297
|
module.exports['Coverage: setSocketHandlers removes a lingering connect error handler'] = test => {
|
|
306
298
|
let client = makeClient();
|
|
307
299
|
let removed = null;
|
|
@@ -389,7 +381,9 @@ module.exports['Coverage: _socketTimeout recovers an IDLE connection with NOOP']
|
|
|
389
381
|
await drain();
|
|
390
382
|
await drain();
|
|
391
383
|
test.ok(noopRun, 'NOOP issued to recover IDLE');
|
|
392
|
-
|
|
384
|
+
// Restarting IDLE is autoidle()'s decision once the NOOP settles (run() re-arms it);
|
|
385
|
+
// the watchdog handler itself must not bypass the busy guard by calling idle() directly.
|
|
386
|
+
test.equal(idleResumed, false, 'the handler does not restart IDLE by itself');
|
|
393
387
|
test.done();
|
|
394
388
|
};
|
|
395
389
|
|
|
@@ -9,6 +9,7 @@ const libbase64 = require('libbase64');
|
|
|
9
9
|
const libqp = require('libqp');
|
|
10
10
|
const libmime = require('libmime');
|
|
11
11
|
const { Writable, finished } = require('stream');
|
|
12
|
+
const { chunkedFetchOne } = require('./fixtures/test-client');
|
|
12
13
|
|
|
13
14
|
const makeClient = (overrides = {}) => {
|
|
14
15
|
let client = new ImapFlow({
|
|
@@ -217,11 +218,7 @@ module.exports['Download: returns empty object without mailbox'] = async test =>
|
|
|
217
218
|
module.exports['Download: full message in multiple chunks'] = async test => {
|
|
218
219
|
let client = makeClient();
|
|
219
220
|
let body = Buffer.from('A'.repeat(10));
|
|
220
|
-
client.fetchOne =
|
|
221
|
-
let start = query.source.start;
|
|
222
|
-
let maxLength = query.source.maxLength;
|
|
223
|
-
return { uid: 1, size: body.length, source: body.slice(start, start + maxLength) };
|
|
224
|
-
};
|
|
221
|
+
client.fetchOne = chunkedFetchOne(body);
|
|
225
222
|
let { meta, content } = await client.download('1', false, { chunkSize: 4 });
|
|
226
223
|
test.equal(meta.contentType, 'message/rfc822');
|
|
227
224
|
test.equal(meta.expectedSize, 10);
|
|
@@ -233,11 +230,7 @@ module.exports['Download: full message in multiple chunks'] = async test => {
|
|
|
233
230
|
module.exports['Download: falls back to default chunkSize/maxBytes when zero'] = async test => {
|
|
234
231
|
let client = makeClient();
|
|
235
232
|
let body = Buffer.from('tiny');
|
|
236
|
-
client.fetchOne =
|
|
237
|
-
let start = query.source.start;
|
|
238
|
-
let maxLength = query.source.maxLength;
|
|
239
|
-
return { uid: 1, size: body.length, source: body.slice(start, start + maxLength) };
|
|
240
|
-
};
|
|
233
|
+
client.fetchOne = chunkedFetchOne(body);
|
|
241
234
|
// zero values are falsy -> the (|| default) fallbacks kick in
|
|
242
235
|
let { content } = await client.download('1', false, { chunkSize: 0, maxBytes: 0 });
|
|
243
236
|
let data = await collect(content);
|
|
@@ -6,17 +6,8 @@
|
|
|
6
6
|
// and the untaggedFetch flag/modseq branches.
|
|
7
7
|
|
|
8
8
|
const { ImapFlow } = require('../lib/imap-flow');
|
|
9
|
-
|
|
10
|
-
const makeClient
|
|
11
|
-
let client = new ImapFlow({
|
|
12
|
-
host: 'imap.example.com',
|
|
13
|
-
port: 993,
|
|
14
|
-
auth: { user: 'test', pass: 'test' },
|
|
15
|
-
logger: false,
|
|
16
|
-
...overrides
|
|
17
|
-
});
|
|
18
|
-
return client;
|
|
19
|
-
};
|
|
9
|
+
const { withFakeTimers } = require('./fixtures/fake-timers');
|
|
10
|
+
const { makeClient, makeIdleReadyClient } = require('./fixtures/test-client');
|
|
20
11
|
|
|
21
12
|
// ============================================================================
|
|
22
13
|
// emitError
|
|
@@ -179,27 +170,79 @@ module.exports['Internals: write returns false for non-string non-buffer'] = tes
|
|
|
179
170
|
test.done();
|
|
180
171
|
};
|
|
181
172
|
|
|
182
|
-
|
|
173
|
+
// A writable client whose raw traffic log is captured, for the two logRaw branches
|
|
174
|
+
const makeRawLogClient = rawSensitiveCommand => {
|
|
183
175
|
let logs = [];
|
|
184
|
-
let client = makeClient({ logRaw: true });
|
|
185
|
-
client.log = {
|
|
186
|
-
trace: o => logs.push(o),
|
|
187
|
-
debug: () => {},
|
|
188
|
-
warn: () => {},
|
|
189
|
-
error: () => {},
|
|
190
|
-
info: () => {}
|
|
191
|
-
};
|
|
192
176
|
let written = [];
|
|
177
|
+
let client = makeClient({ logRaw: true });
|
|
178
|
+
client.log = { trace: o => logs.push(o), debug: () => {}, warn: () => {}, error: () => {}, info: () => {} };
|
|
193
179
|
client.socket = { destroyed: false };
|
|
194
180
|
client.writeSocket = { destroyed: false, write: c => written.push(c) };
|
|
195
|
-
client.
|
|
196
|
-
client
|
|
181
|
+
client.rawSensitiveCommand = rawSensitiveCommand;
|
|
182
|
+
return { client, logs, written };
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
module.exports['Internals: write logs raw data when logRaw enabled'] = test => {
|
|
186
|
+
let { client, logs, written } = makeRawLogClient(false);
|
|
197
187
|
client.write('A NOOP');
|
|
198
|
-
|
|
188
|
+
let entry = logs.find(l => l.src === 'c' && l.msg === 'write to socket');
|
|
189
|
+
test.ok(entry);
|
|
190
|
+
test.equal(Buffer.from(entry.data, 'base64').toString(), 'A NOOP\r\n');
|
|
191
|
+
test.ok(!entry.hidden);
|
|
199
192
|
test.equal(written.length, 1);
|
|
200
193
|
test.done();
|
|
201
194
|
};
|
|
202
195
|
|
|
196
|
+
module.exports['Internals: write withholds raw data for a credential-bearing command'] = test => {
|
|
197
|
+
// send() sets this for LOGIN/AUTHENTICATE before the first frame reaches the socket
|
|
198
|
+
let { client, logs, written } = makeRawLogClient(true);
|
|
199
|
+
client.write('A1 LOGIN "user" "hunter2"');
|
|
200
|
+
let entry = logs.find(l => l.src === 'c' && l.msg === 'write to socket');
|
|
201
|
+
test.ok(entry);
|
|
202
|
+
test.ok(entry.hidden);
|
|
203
|
+
// The placeholder is fixed width, so the entry cannot disclose the password length
|
|
204
|
+
test.equal(Buffer.from(entry.data, 'base64').toString(), '(* value hidden *)\r\n');
|
|
205
|
+
// The frame itself is still written to the socket unchanged
|
|
206
|
+
test.equal(written[0].toString(), 'A1 LOGIN "user" "hunter2"\r\n');
|
|
207
|
+
test.done();
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
module.exports['Internals: send marks credential-bearing commands for the raw log'] = async test => {
|
|
211
|
+
let client = makeClient();
|
|
212
|
+
let written = [];
|
|
213
|
+
client.socket = { destroyed: false };
|
|
214
|
+
client.writeSocket = { destroyed: false, write: c => written.push(c) };
|
|
215
|
+
|
|
216
|
+
// Lower case on purpose: the wire protocol is case-insensitive and exec() passes the
|
|
217
|
+
// caller's spelling through unchanged, so the classification must normalize it
|
|
218
|
+
await client.send({
|
|
219
|
+
tag: 'A1',
|
|
220
|
+
command: 'login',
|
|
221
|
+
attributes: [
|
|
222
|
+
{ type: 'STRING', value: 'user' },
|
|
223
|
+
{ type: 'STRING', value: 'hunter2', sensitive: true }
|
|
224
|
+
],
|
|
225
|
+
options: {}
|
|
226
|
+
});
|
|
227
|
+
test.equal(client.rawSensitiveCommand, true);
|
|
228
|
+
|
|
229
|
+
await client.send({ tag: 'A2', command: 'NOOP', attributes: [], options: {} });
|
|
230
|
+
test.equal(client.rawSensitiveCommand, false);
|
|
231
|
+
|
|
232
|
+
// A command outside the list still masks if it marks an attribute sensitive, so the
|
|
233
|
+
// declarative marker alone is enough to keep a new command out of the raw log. Nested
|
|
234
|
+
// because the command compiler honors the marker at any depth.
|
|
235
|
+
await client.send({
|
|
236
|
+
tag: 'A3',
|
|
237
|
+
command: 'SETMETADATA',
|
|
238
|
+
attributes: [{ type: 'ATOM', value: 'INBOX' }, [{ type: 'STRING', value: 'token', sensitive: true }]],
|
|
239
|
+
options: {}
|
|
240
|
+
});
|
|
241
|
+
test.equal(client.rawSensitiveCommand, true);
|
|
242
|
+
|
|
243
|
+
test.done();
|
|
244
|
+
};
|
|
245
|
+
|
|
203
246
|
module.exports['Internals: write appends CRLF only on final part'] = test => {
|
|
204
247
|
let client = makeClient();
|
|
205
248
|
let written = [];
|
|
@@ -275,29 +318,20 @@ module.exports['Internals: autoidle does nothing when not selected'] = test => {
|
|
|
275
318
|
test.done();
|
|
276
319
|
};
|
|
277
320
|
|
|
278
|
-
module.exports['Internals: autoidle schedules idle when selected'] = test => {
|
|
279
|
-
|
|
280
|
-
|
|
321
|
+
module.exports['Internals: autoidle schedules idle when selected'] = async test => {
|
|
322
|
+
await withFakeTimers(async timers => {
|
|
323
|
+
let client = makeIdleReadyClient();
|
|
324
|
+
|
|
325
|
+
let idleCalled = false;
|
|
326
|
+
client.idle = async () => {
|
|
327
|
+
idleCalled = true;
|
|
328
|
+
};
|
|
281
329
|
|
|
282
|
-
let realSetTimeout = global.setTimeout;
|
|
283
|
-
let idleCalled = false;
|
|
284
|
-
client.idle = async () => {
|
|
285
|
-
idleCalled = true;
|
|
286
|
-
};
|
|
287
|
-
// Intercept the 15s idle timer and fire it synchronously
|
|
288
|
-
global.setTimeout = (fn, ms) => {
|
|
289
|
-
if (ms === 15 * 1000) {
|
|
290
|
-
fn();
|
|
291
|
-
return { unref() {} };
|
|
292
|
-
}
|
|
293
|
-
return realSetTimeout(fn, ms);
|
|
294
|
-
};
|
|
295
|
-
try {
|
|
296
330
|
client.autoidle();
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
331
|
+
await timers.fire();
|
|
332
|
+
|
|
333
|
+
test.ok(idleCalled);
|
|
334
|
+
});
|
|
301
335
|
test.done();
|
|
302
336
|
};
|
|
303
337
|
|
|
@@ -322,7 +356,7 @@ module.exports['Internals: getLogger uses provided logger object'] = test => {
|
|
|
322
356
|
};
|
|
323
357
|
|
|
324
358
|
module.exports['Internals: getLogger falls back to console for missing fatal/error level'] = test => {
|
|
325
|
-
// Logger object missing the 'error' method -> falls through to console.
|
|
359
|
+
// Logger object missing the 'error' method -> falls through to console.error
|
|
326
360
|
let partial = {
|
|
327
361
|
trace() {},
|
|
328
362
|
debug() {},
|
|
@@ -331,15 +365,99 @@ module.exports['Internals: getLogger falls back to console for missing fatal/err
|
|
|
331
365
|
// no error, no fatal
|
|
332
366
|
};
|
|
333
367
|
let client = makeClient({ logger: partial });
|
|
334
|
-
let
|
|
368
|
+
let origConsoleError = console.error;
|
|
335
369
|
let logged = [];
|
|
336
|
-
console.
|
|
370
|
+
console.error = (...args) => logged.push(args);
|
|
337
371
|
try {
|
|
338
|
-
|
|
372
|
+
let err = new Error('boom failure');
|
|
373
|
+
err.code = 'XBOOM';
|
|
374
|
+
// The answer is often one level down: this library attaches the underlying failure
|
|
375
|
+
// as an enumerable `_err`
|
|
376
|
+
err._err = Object.assign(new Error('inner failure'), { code: 'ECONNREFUSED' });
|
|
377
|
+
client.log.error({ msg: 'boom', err });
|
|
378
|
+
// A circular structure must not throw out of the log call, and must not be dropped
|
|
379
|
+
let circular = { msg: 'loop' };
|
|
380
|
+
circular.self = circular;
|
|
381
|
+
client.log.error(circular);
|
|
339
382
|
} finally {
|
|
340
|
-
console.
|
|
383
|
+
console.error = origConsoleError;
|
|
341
384
|
}
|
|
342
|
-
test.
|
|
385
|
+
test.equal(logged.length, 2);
|
|
386
|
+
// The Error was flattened, so message, stack and enumerable fields survive stringify
|
|
387
|
+
let entry = JSON.parse(logged[0][0]);
|
|
388
|
+
test.equal(entry.msg, 'boom');
|
|
389
|
+
test.equal(entry.err.message, 'boom failure');
|
|
390
|
+
test.equal(entry.err.code, 'XBOOM');
|
|
391
|
+
test.ok(entry.err.stack);
|
|
392
|
+
test.equal(entry.err._err.message, 'inner failure');
|
|
393
|
+
test.equal(entry.err._err.code, 'ECONNREFUSED');
|
|
394
|
+
// Unserializable entries still reach console.error, just not as JSON
|
|
395
|
+
test.equal(logged[1][0].msg, 'loop');
|
|
396
|
+
test.done();
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
module.exports['Internals: getLogger keeps cause and AggregateError members'] = test => {
|
|
400
|
+
let client = makeClient({ emitLogs: true });
|
|
401
|
+
let entries = [];
|
|
402
|
+
client.on('log', entry => entries.push(entry));
|
|
403
|
+
|
|
404
|
+
let inner = Object.assign(new Error('inner failure'), { code: 'ECONNREFUSED' });
|
|
405
|
+
client.log.error({ msg: 'wrapped', err: new Error('outer failure', { cause: inner }) });
|
|
406
|
+
// Node reports a multi-address connect failure as an AggregateError
|
|
407
|
+
client.log.error({ msg: 'aggregate', err: new AggregateError([inner], 'all attempts failed') });
|
|
408
|
+
|
|
409
|
+
test.equal(entries[0].err.cause.message, 'inner failure');
|
|
410
|
+
test.equal(entries[0].err.cause.code, 'ECONNREFUSED');
|
|
411
|
+
test.equal(entries[1].err.errors.length, 1);
|
|
412
|
+
test.equal(entries[1].err.errors[0].message, 'inner failure');
|
|
413
|
+
test.done();
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
module.exports['Internals: getLogger bounds a looping and a deep error chain'] = test => {
|
|
417
|
+
let client = makeClient({ emitLogs: true });
|
|
418
|
+
let entries = [];
|
|
419
|
+
client.on('log', entry => entries.push(entry));
|
|
420
|
+
|
|
421
|
+
// A chain that loops back must terminate rather than recurse forever
|
|
422
|
+
let looping = new Error('looping failure');
|
|
423
|
+
looping._err = looping;
|
|
424
|
+
client.log.error({ msg: 'loop', err: looping });
|
|
425
|
+
test.equal(entries[0].err.message, 'looping failure');
|
|
426
|
+
test.equal(entries[0].err._err, 'looping failure');
|
|
427
|
+
|
|
428
|
+
// A chain longer than the depth cap is truncated rather than walked to the end
|
|
429
|
+
let deep = new Error('level 0');
|
|
430
|
+
for (let i = 1; i <= 6; i++) {
|
|
431
|
+
deep = Object.assign(new Error(`level ${i}`), { _err: deep });
|
|
432
|
+
}
|
|
433
|
+
client.log.error({ msg: 'deep', err: deep });
|
|
434
|
+
test.equal(entries[1].err._err._err._err.message, 'level 3');
|
|
435
|
+
// Past the cap the chain collapses to messages instead of being walked to the end
|
|
436
|
+
test.equal(entries[1].err._err._err._err._err, 'level 2');
|
|
437
|
+
|
|
438
|
+
test.done();
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
module.exports['Internals: getLogger never throws out of a log call'] = test => {
|
|
442
|
+
let client = makeClient({ emitLogs: true });
|
|
443
|
+
let entries = [];
|
|
444
|
+
client.on('log', entry => entries.push(entry));
|
|
445
|
+
|
|
446
|
+
// A throwing property getter on the logged error must not escape
|
|
447
|
+
let hostile = {
|
|
448
|
+
get message() {
|
|
449
|
+
throw new Error('getter blew up');
|
|
450
|
+
},
|
|
451
|
+
stack: 'x'
|
|
452
|
+
};
|
|
453
|
+
test.doesNotThrow(() => client.log.warn({ msg: 'hostile', err: hostile }));
|
|
454
|
+
|
|
455
|
+
// Neither must a throwing 'log' listener
|
|
456
|
+
client.on('log', () => {
|
|
457
|
+
throw new Error('listener blew up');
|
|
458
|
+
});
|
|
459
|
+
test.doesNotThrow(() => client.log.warn({ msg: 'still fine' }));
|
|
460
|
+
|
|
343
461
|
test.done();
|
|
344
462
|
};
|
|
345
463
|
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
// Asserted through timer identity and cleanup rather than wall-clock sleeps.
|
|
10
10
|
|
|
11
11
|
const net = require('net');
|
|
12
|
-
const { ImapFlow } = require('../lib/imap-flow');
|
|
13
12
|
const idleCommand = require('../lib/commands/idle.js');
|
|
14
13
|
const { withFakeTimers } = require('./fixtures/fake-timers');
|
|
14
|
+
const { makeClient, makeIdleReadyClient } = require('./fixtures/test-client');
|
|
15
15
|
|
|
16
16
|
const CAPS = 'IMAP4rev1 ID ENABLE NAMESPACE IDLE';
|
|
17
17
|
|
|
@@ -52,15 +52,6 @@ const createServer = () =>
|
|
|
52
52
|
|
|
53
53
|
const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve(server.address().port)));
|
|
54
54
|
|
|
55
|
-
const makeClient = (overrides = {}) =>
|
|
56
|
-
new ImapFlow({
|
|
57
|
-
host: '127.0.0.1',
|
|
58
|
-
port: 993,
|
|
59
|
-
logger: false,
|
|
60
|
-
auth: { user: 'test', pass: 'secret' },
|
|
61
|
-
...overrides
|
|
62
|
-
});
|
|
63
|
-
|
|
64
55
|
module.exports['Timers: connection and greeting deadlines keep the process alive'] = async test => {
|
|
65
56
|
let server = createServer();
|
|
66
57
|
let port = await listen(server);
|
|
@@ -99,15 +90,13 @@ module.exports['Timers: connection and greeting deadlines keep the process alive
|
|
|
99
90
|
|
|
100
91
|
module.exports['Timers: the auto-IDLE timer is unrefd and cleared on close'] = async test => {
|
|
101
92
|
await withFakeTimers(async timers => {
|
|
102
|
-
let client =
|
|
103
|
-
client.state = client.states.SELECTED;
|
|
104
|
-
client.idle = async () => {};
|
|
93
|
+
let client = makeIdleReadyClient();
|
|
105
94
|
|
|
106
95
|
client.autoidle();
|
|
107
96
|
|
|
108
97
|
let armed = timers.pending();
|
|
109
98
|
test.equal(armed.length, 1, 'exactly one auto-IDLE timer is armed');
|
|
110
|
-
test.equal(armed[0].delay,
|
|
99
|
+
test.equal(armed[0].delay, client.autoIdleDelay);
|
|
111
100
|
test.ok(armed[0].unrefd, 'the background auto-IDLE timer does not keep the process alive');
|
|
112
101
|
|
|
113
102
|
client.close();
|
|
@@ -119,9 +108,7 @@ module.exports['Timers: the auto-IDLE timer is unrefd and cleared on close'] = a
|
|
|
119
108
|
|
|
120
109
|
module.exports['Timers: a restarted auto-IDLE timer replaces the previous one'] = async test => {
|
|
121
110
|
await withFakeTimers(async timers => {
|
|
122
|
-
let client =
|
|
123
|
-
client.state = client.states.SELECTED;
|
|
124
|
-
client.idle = async () => {};
|
|
111
|
+
let client = makeIdleReadyClient();
|
|
125
112
|
|
|
126
113
|
client.autoidle();
|
|
127
114
|
client.autoidle();
|