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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/lib/commands/authenticate.js +5 -5
- package/lib/commands/fetch.js +1 -0
- package/lib/commands/logout.js +1 -0
- package/lib/commands/select.js +2 -1
- package/lib/commands/store.js +1 -0
- package/lib/handler/token-parser.js +0 -5
- package/lib/imap-flow.js +36 -6
- package/lib/tools.js +8 -0
- package/package.json +1 -1
- package/test/commands-branches-test.js +1068 -0
- package/test/fixtures/test-tls.js +8 -0
- package/test/handler-branches-test.js +334 -0
- package/test/imap-compiler-test.js +47 -0
- package/test/imap-flow-compress-test.js +154 -0
- package/test/imap-flow-coverage-test.js +609 -0
- package/test/imap-flow-fetch-download-test.js +801 -0
- package/test/imap-flow-internals-test.js +450 -0
- package/test/imap-flow-methods-test.js +738 -0
- package/test/imap-flow-proxy-paths-test.js +215 -0
- package/test/imap-flow-secure-test.js +327 -0
- package/test/imap-flow-server-test.js +1046 -0
- package/test/imap-formal-syntax-test.js +18 -0
- package/test/imap-parser-test.js +52 -0
- package/test/imap-stream-edge-cases-test.js +64 -0
- package/test/jp-decoder-test.js +48 -0
- package/test/limited-passthrough-test.js +17 -0
- package/test/search-compiler-test.js +31 -0
- package/test/search-test.js +61 -0
- package/test/token-parser-test.js +64 -0
- package/test/tools-test.js +360 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Targeted unit tests for low-level ImapFlow internals that are awkward to reach
|
|
4
|
+
// through a full session: emitError routing, write() guards, run() dispatch
|
|
5
|
+
// guards, the synthetic logger, the streamer error handler, autoidle scheduling
|
|
6
|
+
// and the untaggedFetch flag/modseq branches.
|
|
7
|
+
|
|
8
|
+
const { ImapFlow } = require('../lib/imap-flow');
|
|
9
|
+
|
|
10
|
+
const makeClient = (overrides = {}) => {
|
|
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
|
+
};
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// emitError
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
module.exports['Internals: emitError ignores falsy error'] = test => {
|
|
26
|
+
let client = makeClient();
|
|
27
|
+
let emitted = false;
|
|
28
|
+
client.on('error', () => {
|
|
29
|
+
emitted = true;
|
|
30
|
+
});
|
|
31
|
+
client.emitError(null);
|
|
32
|
+
test.equal(emitted, false);
|
|
33
|
+
test.done();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
module.exports['Internals: emitError routes to upgrade rejector while upgrading'] = test => {
|
|
37
|
+
let client = makeClient();
|
|
38
|
+
client.socket = { destroyed: true, destroy: () => {} };
|
|
39
|
+
client.upgrading = true;
|
|
40
|
+
let rejected = null;
|
|
41
|
+
client._upgradeReject = err => {
|
|
42
|
+
rejected = err;
|
|
43
|
+
};
|
|
44
|
+
let err = new Error('tls boom');
|
|
45
|
+
client.emitError(err);
|
|
46
|
+
test.equal(rejected, err);
|
|
47
|
+
test.equal(client.upgrading, false);
|
|
48
|
+
test.done();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports['Internals: emitError rejects pending connect promise'] = test => {
|
|
52
|
+
let client = makeClient();
|
|
53
|
+
client.socket = { destroyed: true, destroy: () => {} };
|
|
54
|
+
let rejected = null;
|
|
55
|
+
client.initialReject = err => {
|
|
56
|
+
rejected = err;
|
|
57
|
+
};
|
|
58
|
+
client.initialResolve = () => {};
|
|
59
|
+
let err = new Error('connect boom');
|
|
60
|
+
client.emitError(err);
|
|
61
|
+
test.equal(rejected, err);
|
|
62
|
+
test.equal(client.initialReject, false);
|
|
63
|
+
test.done();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
module.exports['Internals: emitError emits error event as fallback'] = test => {
|
|
67
|
+
let client = makeClient();
|
|
68
|
+
client.socket = { destroyed: true, destroy: () => {} };
|
|
69
|
+
let emitted = null;
|
|
70
|
+
client.on('error', err => {
|
|
71
|
+
emitted = err;
|
|
72
|
+
});
|
|
73
|
+
let err = new Error('plain boom');
|
|
74
|
+
client.emitError(err);
|
|
75
|
+
test.equal(emitted, err);
|
|
76
|
+
test.equal(err._connId, client.id);
|
|
77
|
+
test.done();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// streamer error handler
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
module.exports['Internals: streamer error handler silently closes on transient codes'] = test => {
|
|
85
|
+
let client = makeClient();
|
|
86
|
+
let closeAfterCalled = false;
|
|
87
|
+
client.closeAfter = () => {
|
|
88
|
+
closeAfterCalled = true;
|
|
89
|
+
};
|
|
90
|
+
let emitted = false;
|
|
91
|
+
client.on('error', () => {
|
|
92
|
+
emitted = true;
|
|
93
|
+
});
|
|
94
|
+
client._streamerErrorHandler({ code: 'ECONNRESET', message: 'reset' });
|
|
95
|
+
test.ok(closeAfterCalled);
|
|
96
|
+
test.equal(emitted, false);
|
|
97
|
+
test.done();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
module.exports['Internals: streamer error handler emits on other codes'] = test => {
|
|
101
|
+
let client = makeClient();
|
|
102
|
+
client.socket = { destroyed: true, destroy: () => {} };
|
|
103
|
+
let emitted = null;
|
|
104
|
+
client.on('error', err => {
|
|
105
|
+
emitted = err;
|
|
106
|
+
});
|
|
107
|
+
client._streamerErrorHandler({ code: 'EOTHER', message: 'weird' });
|
|
108
|
+
test.ok(emitted);
|
|
109
|
+
test.done();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// write()
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
module.exports['Internals: write throws when socket destroyed'] = test => {
|
|
117
|
+
let client = makeClient();
|
|
118
|
+
client.socket = { destroyed: true };
|
|
119
|
+
test.throws(() => client.write('A NOOP'), /Socket is already closed/);
|
|
120
|
+
test.done();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
module.exports['Internals: write throws after logout'] = test => {
|
|
124
|
+
let client = makeClient();
|
|
125
|
+
client.socket = { destroyed: false };
|
|
126
|
+
client.state = client.states.LOGOUT;
|
|
127
|
+
test.throws(() => client.write('A NOOP'), /Can not send data after logged out/);
|
|
128
|
+
test.done();
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
module.exports['Internals: write closes when writeSocket destroyed'] = test => {
|
|
132
|
+
let client = makeClient();
|
|
133
|
+
client.socket = { destroyed: false };
|
|
134
|
+
client.writeSocket = { destroyed: true };
|
|
135
|
+
client.state = client.states.AUTHENTICATED;
|
|
136
|
+
let closed = false;
|
|
137
|
+
client.close = () => {
|
|
138
|
+
closed = true;
|
|
139
|
+
};
|
|
140
|
+
let res = client.write('A NOOP');
|
|
141
|
+
test.equal(res, undefined);
|
|
142
|
+
test.ok(closed);
|
|
143
|
+
test.done();
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
module.exports['Internals: write returns false for non-string non-buffer'] = test => {
|
|
147
|
+
let client = makeClient();
|
|
148
|
+
let written = [];
|
|
149
|
+
client.socket = { destroyed: false };
|
|
150
|
+
client.writeSocket = { destroyed: false, write: c => written.push(c) };
|
|
151
|
+
client.state = client.states.AUTHENTICATED;
|
|
152
|
+
client.commandParts = [];
|
|
153
|
+
let res = client.write({ not: 'a buffer' });
|
|
154
|
+
test.equal(res, false);
|
|
155
|
+
test.equal(written.length, 0);
|
|
156
|
+
test.done();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
module.exports['Internals: write logs raw data when logRaw enabled'] = test => {
|
|
160
|
+
let logs = [];
|
|
161
|
+
let client = makeClient({ logRaw: true });
|
|
162
|
+
client.log = {
|
|
163
|
+
trace: o => logs.push(o),
|
|
164
|
+
debug: () => {},
|
|
165
|
+
warn: () => {},
|
|
166
|
+
error: () => {},
|
|
167
|
+
info: () => {}
|
|
168
|
+
};
|
|
169
|
+
let written = [];
|
|
170
|
+
client.socket = { destroyed: false };
|
|
171
|
+
client.writeSocket = { destroyed: false, write: c => written.push(c) };
|
|
172
|
+
client.state = client.states.AUTHENTICATED;
|
|
173
|
+
client.commandParts = [];
|
|
174
|
+
client.write('A NOOP');
|
|
175
|
+
test.ok(logs.some(l => l.src === 'c' && l.msg === 'write to socket'));
|
|
176
|
+
test.equal(written.length, 1);
|
|
177
|
+
test.done();
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
module.exports['Internals: write appends CRLF only on final part'] = test => {
|
|
181
|
+
let client = makeClient();
|
|
182
|
+
let written = [];
|
|
183
|
+
client.socket = { destroyed: false };
|
|
184
|
+
client.writeSocket = { destroyed: false, write: c => written.push(c) };
|
|
185
|
+
client.state = client.states.AUTHENTICATED;
|
|
186
|
+
// Pending command parts => no CRLF appended to this chunk
|
|
187
|
+
client.commandParts = ['more'];
|
|
188
|
+
client.write(Buffer.from('literal'));
|
|
189
|
+
test.equal(written[0].toString(), 'literal');
|
|
190
|
+
test.done();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// run()
|
|
195
|
+
// ============================================================================
|
|
196
|
+
|
|
197
|
+
module.exports['Internals: run returns false for unknown command'] = async test => {
|
|
198
|
+
let client = makeClient();
|
|
199
|
+
client.socket = { destroyed: false };
|
|
200
|
+
let res = await client.run('NOT_A_COMMAND');
|
|
201
|
+
test.equal(res, false);
|
|
202
|
+
test.done();
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
module.exports['Internals: run throws NoConnection without socket'] = async test => {
|
|
206
|
+
let client = makeClient();
|
|
207
|
+
client.socket = null;
|
|
208
|
+
let err = null;
|
|
209
|
+
try {
|
|
210
|
+
await client.run('NOOP');
|
|
211
|
+
} catch (e) {
|
|
212
|
+
err = e;
|
|
213
|
+
}
|
|
214
|
+
test.ok(err);
|
|
215
|
+
test.equal(err.code, 'NoConnection');
|
|
216
|
+
test.done();
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
module.exports['Internals: run invokes preCheck and command handler'] = async test => {
|
|
220
|
+
let client = makeClient();
|
|
221
|
+
client.socket = { destroyed: false };
|
|
222
|
+
let preCheckCalled = false;
|
|
223
|
+
client.preCheck = async () => {
|
|
224
|
+
preCheckCalled = true;
|
|
225
|
+
};
|
|
226
|
+
let handlerArgs = null;
|
|
227
|
+
client.commands = new Map([
|
|
228
|
+
[
|
|
229
|
+
'TESTCMD',
|
|
230
|
+
async (conn, a, b) => {
|
|
231
|
+
handlerArgs = [a, b];
|
|
232
|
+
return 'handled';
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
]);
|
|
236
|
+
let res = await client.run('TESTCMD', 1, 2);
|
|
237
|
+
test.equal(res, 'handled');
|
|
238
|
+
test.ok(preCheckCalled);
|
|
239
|
+
test.deepEqual(handlerArgs, [1, 2]);
|
|
240
|
+
test.done();
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// ============================================================================
|
|
244
|
+
// autoidle
|
|
245
|
+
// ============================================================================
|
|
246
|
+
|
|
247
|
+
module.exports['Internals: autoidle does nothing when not selected'] = test => {
|
|
248
|
+
let client = makeClient();
|
|
249
|
+
client.state = client.states.AUTHENTICATED;
|
|
250
|
+
client.autoidle();
|
|
251
|
+
test.equal(client.idleStartTimer, undefined);
|
|
252
|
+
test.done();
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
module.exports['Internals: autoidle schedules idle when selected'] = test => {
|
|
256
|
+
let client = makeClient();
|
|
257
|
+
client.state = client.states.SELECTED;
|
|
258
|
+
|
|
259
|
+
let realSetTimeout = global.setTimeout;
|
|
260
|
+
let idleCalled = false;
|
|
261
|
+
client.idle = async () => {
|
|
262
|
+
idleCalled = true;
|
|
263
|
+
};
|
|
264
|
+
// Intercept the 15s idle timer and fire it synchronously
|
|
265
|
+
global.setTimeout = (fn, ms) => {
|
|
266
|
+
if (ms === 15 * 1000) {
|
|
267
|
+
fn();
|
|
268
|
+
return { unref() {} };
|
|
269
|
+
}
|
|
270
|
+
return realSetTimeout(fn, ms);
|
|
271
|
+
};
|
|
272
|
+
try {
|
|
273
|
+
client.autoidle();
|
|
274
|
+
} finally {
|
|
275
|
+
global.setTimeout = realSetTimeout;
|
|
276
|
+
}
|
|
277
|
+
test.ok(idleCalled);
|
|
278
|
+
test.done();
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// getLogger
|
|
283
|
+
// ============================================================================
|
|
284
|
+
|
|
285
|
+
module.exports['Internals: getLogger uses provided logger object'] = test => {
|
|
286
|
+
let entries = [];
|
|
287
|
+
let custom = {
|
|
288
|
+
trace: o => entries.push(['trace', o]),
|
|
289
|
+
debug: o => entries.push(['debug', o]),
|
|
290
|
+
info: o => entries.push(['info', o]),
|
|
291
|
+
warn: o => entries.push(['warn', o]),
|
|
292
|
+
error: o => entries.push(['error', o]),
|
|
293
|
+
fatal: o => entries.push(['fatal', o])
|
|
294
|
+
};
|
|
295
|
+
let client = makeClient({ logger: custom });
|
|
296
|
+
client.log.info({ msg: 'hello' });
|
|
297
|
+
test.ok(entries.some(e => e[0] === 'info'));
|
|
298
|
+
test.done();
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
module.exports['Internals: getLogger falls back to console for missing fatal/error level'] = test => {
|
|
302
|
+
// Logger object missing the 'error' method -> falls through to console.log
|
|
303
|
+
let partial = {
|
|
304
|
+
trace() {},
|
|
305
|
+
debug() {},
|
|
306
|
+
info() {},
|
|
307
|
+
warn() {}
|
|
308
|
+
// no error, no fatal
|
|
309
|
+
};
|
|
310
|
+
let client = makeClient({ logger: partial });
|
|
311
|
+
let origConsoleLog = console.log;
|
|
312
|
+
let logged = [];
|
|
313
|
+
console.log = (...args) => logged.push(args);
|
|
314
|
+
try {
|
|
315
|
+
client.log.error({ msg: 'boom' });
|
|
316
|
+
} finally {
|
|
317
|
+
console.log = origConsoleLog;
|
|
318
|
+
}
|
|
319
|
+
test.ok(logged.length >= 1);
|
|
320
|
+
test.done();
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
module.exports['Internals: getLogger emits log events when emitLogs set'] = test => {
|
|
324
|
+
let client = makeClient({ logger: false });
|
|
325
|
+
client.emitLogs = true;
|
|
326
|
+
let events = [];
|
|
327
|
+
client.on('log', entry => events.push(entry));
|
|
328
|
+
let err = new Error('with stack');
|
|
329
|
+
err.code = 'XCODE';
|
|
330
|
+
client.log.warn({ msg: 'warned', err });
|
|
331
|
+
test.equal(events.length, 1);
|
|
332
|
+
test.equal(events[0].level, 'warn');
|
|
333
|
+
test.equal(events[0].err.code, 'XCODE');
|
|
334
|
+
test.ok(events[0].err.stack);
|
|
335
|
+
test.done();
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
module.exports['Internals: logger:false suppresses output but still allows log calls'] = test => {
|
|
339
|
+
let client = makeClient({ logger: false });
|
|
340
|
+
// Should not throw
|
|
341
|
+
client.log.debug({ msg: 'nothing happens' });
|
|
342
|
+
test.ok(true);
|
|
343
|
+
test.done();
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// ============================================================================
|
|
347
|
+
// Misc branch coverage
|
|
348
|
+
// ============================================================================
|
|
349
|
+
|
|
350
|
+
module.exports['Internals: secure connection defaults to port 993'] = test => {
|
|
351
|
+
let client = new ImapFlow({ secure: true });
|
|
352
|
+
test.equal(client.port, 993);
|
|
353
|
+
test.done();
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
module.exports['Internals: getUntaggedHandler ignores non-string type token'] = test => {
|
|
357
|
+
let client = makeClient();
|
|
358
|
+
client.untaggedHandlers = { '5': () => 'numeric-keyword-handler' };
|
|
359
|
+
// numeric prefix but attributes[0].value is not a string -> keyword stays '5'
|
|
360
|
+
let handler = client.getUntaggedHandler('5', [{ type: 'ATOM', value: 12345 }]);
|
|
361
|
+
test.equal(handler(), 'numeric-keyword-handler');
|
|
362
|
+
test.done();
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
module.exports['Internals: untaggedVanished filters non-string tag entries'] = async test => {
|
|
366
|
+
let client = makeClient();
|
|
367
|
+
client.mailbox = { path: 'INBOX' };
|
|
368
|
+
let events = [];
|
|
369
|
+
client.on('expunge', e => events.push(e));
|
|
370
|
+
await client.untaggedVanished({
|
|
371
|
+
attributes: [[{ value: 12345 }], { value: '7' }] // non-string tag value filtered out
|
|
372
|
+
});
|
|
373
|
+
test.equal(events.length, 1);
|
|
374
|
+
test.equal(events[0].earlier, false);
|
|
375
|
+
test.done();
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
module.exports['Internals: setFlagColor returns false when STORE yields falsy'] = async test => {
|
|
379
|
+
let client = makeClient();
|
|
380
|
+
client.mailbox = { path: 'INBOX', exists: 5 };
|
|
381
|
+
client.socket = { destroyed: false };
|
|
382
|
+
client.run = async () => false; // both add and remove STORE return falsy
|
|
383
|
+
let res = await client.setFlagColor('1', 'red');
|
|
384
|
+
test.equal(res, false);
|
|
385
|
+
test.done();
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
module.exports['Internals: fetchOne with falsy seq coerces to empty string'] = async test => {
|
|
389
|
+
let client = makeClient();
|
|
390
|
+
client.mailbox = { path: 'INBOX', exists: 5 };
|
|
391
|
+
let captured = null;
|
|
392
|
+
client.run = async (cmd, seq) => {
|
|
393
|
+
captured = seq;
|
|
394
|
+
return { list: [] };
|
|
395
|
+
};
|
|
396
|
+
let res = await client.fetchOne(0, {});
|
|
397
|
+
test.equal(captured, ''); // (0 || '').toString()
|
|
398
|
+
test.equal(res, false);
|
|
399
|
+
test.done();
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
module.exports['Internals: unbind falls back to socket when writeSocket missing'] = test => {
|
|
403
|
+
let client = makeClient();
|
|
404
|
+
let raw = {
|
|
405
|
+
unpipe() {},
|
|
406
|
+
on() {},
|
|
407
|
+
once() {},
|
|
408
|
+
removeListener() {}
|
|
409
|
+
};
|
|
410
|
+
client.socket = raw;
|
|
411
|
+
client.writeSocket = null;
|
|
412
|
+
client.streamer = { /* unused here */ };
|
|
413
|
+
let result = client.unbind();
|
|
414
|
+
test.equal(result.writeSocket, raw);
|
|
415
|
+
test.done();
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// ============================================================================
|
|
419
|
+
// untaggedFetch flag/modseq branches
|
|
420
|
+
// ============================================================================
|
|
421
|
+
|
|
422
|
+
module.exports['Internals: untaggedFetch includes modseq and flagColor'] = async test => {
|
|
423
|
+
let client = makeClient();
|
|
424
|
+
client.mailbox = { path: 'INBOX', exists: 5 };
|
|
425
|
+
let evt = null;
|
|
426
|
+
client.on('flags', e => {
|
|
427
|
+
evt = e;
|
|
428
|
+
});
|
|
429
|
+
let untagged = {
|
|
430
|
+
command: '2',
|
|
431
|
+
attributes: [
|
|
432
|
+
{ type: 'ATOM', value: 'FETCH' },
|
|
433
|
+
[
|
|
434
|
+
{ type: 'ATOM', value: 'UID' },
|
|
435
|
+
{ type: 'ATOM', value: '20' },
|
|
436
|
+
{ type: 'ATOM', value: 'MODSEQ' },
|
|
437
|
+
[{ type: 'ATOM', value: '4242' }],
|
|
438
|
+
{ type: 'ATOM', value: 'FLAGS' },
|
|
439
|
+
[{ type: 'ATOM', value: '\\Flagged' }]
|
|
440
|
+
]
|
|
441
|
+
]
|
|
442
|
+
};
|
|
443
|
+
await client.untaggedFetch(untagged);
|
|
444
|
+
test.ok(evt);
|
|
445
|
+
test.equal(evt.uid, 20);
|
|
446
|
+
test.ok(evt.modseq);
|
|
447
|
+
// \\Flagged with no MailFlagBit keywords maps to the red color
|
|
448
|
+
test.equal(evt.flagColor, 'red');
|
|
449
|
+
test.done();
|
|
450
|
+
};
|