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,1046 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// End-to-end ImapFlow tests against a scriptable in-process mock IMAP server.
|
|
4
|
+
// This exercises the connection lifecycle that pure unit tests cannot reach:
|
|
5
|
+
// connect/greeting handling, startSession (CAPABILITY/ID/NAMESPACE/ENABLE),
|
|
6
|
+
// authentication, the reader loop (OK/NO/BAD/untagged/continuation handling),
|
|
7
|
+
// send/trySend/write, socket handlers, autoidle, locks, logout and close.
|
|
8
|
+
|
|
9
|
+
const net = require('net');
|
|
10
|
+
const { ImapFlow } = require('../lib/imap-flow');
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Mock IMAP server
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// `handlers` maps an uppercase command keyword to (ctx) => void, where ctx
|
|
16
|
+
// provides { tag, line, args, write(str), ok(text), no(text), bad(text), socket }.
|
|
17
|
+
// Sensible defaults are provided for a full happy-path session; tests override
|
|
18
|
+
// individual commands as needed.
|
|
19
|
+
const createServer = (options = {}) => {
|
|
20
|
+
const capabilities = options.capabilities || 'IMAP4rev1 ID ENABLE NAMESPACE UIDPLUS CONDSTORE MOVE QUOTA';
|
|
21
|
+
const greeting = options.greeting || `* OK [CAPABILITY ${capabilities}] mock ready\r\n`;
|
|
22
|
+
|
|
23
|
+
const defaults = {
|
|
24
|
+
CAPABILITY(ctx) {
|
|
25
|
+
ctx.write(`* CAPABILITY ${capabilities}\r\n`);
|
|
26
|
+
ctx.ok('CAPABILITY completed');
|
|
27
|
+
},
|
|
28
|
+
ID(ctx) {
|
|
29
|
+
ctx.write('* ID ("name" "mock" "version" "1.0")\r\n');
|
|
30
|
+
ctx.ok('ID completed');
|
|
31
|
+
},
|
|
32
|
+
NAMESPACE(ctx) {
|
|
33
|
+
ctx.write('* NAMESPACE (("" "/")) NIL NIL\r\n');
|
|
34
|
+
ctx.ok('NAMESPACE completed');
|
|
35
|
+
},
|
|
36
|
+
ENABLE(ctx) {
|
|
37
|
+
ctx.write('* ENABLED CONDSTORE\r\n');
|
|
38
|
+
ctx.ok('ENABLE completed');
|
|
39
|
+
},
|
|
40
|
+
LOGIN(ctx) {
|
|
41
|
+
ctx.ok('LOGIN completed');
|
|
42
|
+
},
|
|
43
|
+
COMPRESS(ctx) {
|
|
44
|
+
ctx.no('COMPRESS not available');
|
|
45
|
+
},
|
|
46
|
+
SELECT(ctx) {
|
|
47
|
+
ctx.write('* 3 EXISTS\r\n');
|
|
48
|
+
ctx.write('* 0 RECENT\r\n');
|
|
49
|
+
ctx.write('* FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)\r\n');
|
|
50
|
+
ctx.write('* OK [PERMANENTFLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft \\*)] Limited\r\n');
|
|
51
|
+
ctx.write('* OK [UIDVALIDITY 12345] UIDs valid\r\n');
|
|
52
|
+
ctx.write('* OK [UIDNEXT 100] Predicted next UID\r\n');
|
|
53
|
+
ctx.write('* OK [HIGHESTMODSEQ 1000] Highest\r\n');
|
|
54
|
+
ctx.ok('[READ-WRITE] SELECT completed');
|
|
55
|
+
},
|
|
56
|
+
EXAMINE(ctx) {
|
|
57
|
+
ctx.write('* 3 EXISTS\r\n');
|
|
58
|
+
ctx.write('* OK [UIDVALIDITY 12345] UIDs valid\r\n');
|
|
59
|
+
ctx.write('* OK [UIDNEXT 100] Predicted next UID\r\n');
|
|
60
|
+
ctx.ok('[READ-ONLY] EXAMINE completed');
|
|
61
|
+
},
|
|
62
|
+
LIST(ctx) {
|
|
63
|
+
ctx.write('* LIST (\\HasNoChildren) "/" "INBOX"\r\n');
|
|
64
|
+
ctx.write('* LIST (\\HasNoChildren \\Sent) "/" "Sent"\r\n');
|
|
65
|
+
ctx.ok('LIST completed');
|
|
66
|
+
},
|
|
67
|
+
LSUB(ctx) {
|
|
68
|
+
ctx.write('* LSUB (\\HasNoChildren) "/" "INBOX"\r\n');
|
|
69
|
+
ctx.ok('LSUB completed');
|
|
70
|
+
},
|
|
71
|
+
STATUS(ctx) {
|
|
72
|
+
ctx.write('* STATUS "INBOX" (MESSAGES 3 UIDNEXT 100 UIDVALIDITY 12345 UNSEEN 1)\r\n');
|
|
73
|
+
ctx.ok('STATUS completed');
|
|
74
|
+
},
|
|
75
|
+
NOOP(ctx) {
|
|
76
|
+
ctx.ok('NOOP completed');
|
|
77
|
+
},
|
|
78
|
+
LOGOUT(ctx) {
|
|
79
|
+
ctx.write('* BYE Logging out\r\n');
|
|
80
|
+
ctx.ok('LOGOUT completed');
|
|
81
|
+
},
|
|
82
|
+
SEARCH(ctx) {
|
|
83
|
+
ctx.write('* SEARCH 1 2 3\r\n');
|
|
84
|
+
ctx.ok('SEARCH completed');
|
|
85
|
+
},
|
|
86
|
+
CREATE(ctx) {
|
|
87
|
+
ctx.ok('CREATE completed');
|
|
88
|
+
},
|
|
89
|
+
DELETE(ctx) {
|
|
90
|
+
ctx.ok('DELETE completed');
|
|
91
|
+
},
|
|
92
|
+
RENAME(ctx) {
|
|
93
|
+
ctx.ok('RENAME completed');
|
|
94
|
+
},
|
|
95
|
+
SUBSCRIBE(ctx) {
|
|
96
|
+
ctx.ok('SUBSCRIBE completed');
|
|
97
|
+
},
|
|
98
|
+
UNSUBSCRIBE(ctx) {
|
|
99
|
+
ctx.ok('UNSUBSCRIBE completed');
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handlers = Object.assign({}, defaults, options.handlers || {});
|
|
104
|
+
|
|
105
|
+
const server = net.createServer(socket => {
|
|
106
|
+
socket.setNoDelay(true);
|
|
107
|
+
socket.on('error', () => {});
|
|
108
|
+
if (options.onConnect) {
|
|
109
|
+
options.onConnect(socket);
|
|
110
|
+
}
|
|
111
|
+
if (greeting) {
|
|
112
|
+
socket.write(greeting);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let buf = Buffer.alloc(0);
|
|
116
|
+
let literalRemaining = 0;
|
|
117
|
+
let cmdPrefix = '';
|
|
118
|
+
|
|
119
|
+
const dispatch = fullLine => {
|
|
120
|
+
// fullLine is the first physical line of the command (tag + command + args);
|
|
121
|
+
// literal payloads are not needed to choose a response.
|
|
122
|
+
let parts = fullLine.split(' ');
|
|
123
|
+
let tag = parts[0];
|
|
124
|
+
let command = (parts[1] || '').toUpperCase();
|
|
125
|
+
// IDLE is terminated by a bare, untagged "DONE" continuation line, so it
|
|
126
|
+
// has no tag/command split — route it to the DONE handler explicitly.
|
|
127
|
+
if (!parts[1] && (parts[0] || '').toUpperCase() === 'DONE') {
|
|
128
|
+
command = 'DONE';
|
|
129
|
+
}
|
|
130
|
+
let args = parts.slice(2).join(' ');
|
|
131
|
+
|
|
132
|
+
const ctx = {
|
|
133
|
+
tag,
|
|
134
|
+
command,
|
|
135
|
+
line: fullLine,
|
|
136
|
+
args,
|
|
137
|
+
socket,
|
|
138
|
+
write: str => socket.write(str),
|
|
139
|
+
ok: text => socket.write(`${tag} OK ${text || 'completed'}\r\n`),
|
|
140
|
+
no: text => socket.write(`${tag} NO ${text || 'failed'}\r\n`),
|
|
141
|
+
bad: text => socket.write(`${tag} BAD ${text || 'bad'}\r\n`)
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
let handler = handlers[command];
|
|
145
|
+
if (typeof handler === 'function') {
|
|
146
|
+
handler(ctx);
|
|
147
|
+
} else if (handler === null) {
|
|
148
|
+
// explicitly silent (e.g. simulate no response)
|
|
149
|
+
} else {
|
|
150
|
+
ctx.bad(`Unknown command ${command}`);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const processBuffer = () => {
|
|
155
|
+
// Loop until we run out of complete lines / literal data.
|
|
156
|
+
while (true) {
|
|
157
|
+
if (literalRemaining > 0) {
|
|
158
|
+
if (buf.length < literalRemaining) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
buf = buf.slice(literalRemaining);
|
|
162
|
+
literalRemaining = 0;
|
|
163
|
+
// Fall through to read the continuation line (post-literal text + CRLF)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let idx = buf.indexOf('\r\n');
|
|
167
|
+
if (idx < 0) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
let line = buf.slice(0, idx).toString('binary');
|
|
171
|
+
buf = buf.slice(idx + 2);
|
|
172
|
+
|
|
173
|
+
let combined = cmdPrefix + line;
|
|
174
|
+
|
|
175
|
+
// Synchronizing / non-synchronizing literal at end of line?
|
|
176
|
+
let m = combined.match(/\{(\d+)(\+)?\}$/);
|
|
177
|
+
if (m) {
|
|
178
|
+
literalRemaining = Number(m[1]);
|
|
179
|
+
// Keep the prefix (sans literal marker) for command keyword extraction
|
|
180
|
+
cmdPrefix = combined.replace(/\{(\d+)(\+)?\}$/, '<literal> ');
|
|
181
|
+
if (!m[2]) {
|
|
182
|
+
// synchronizing literal -> tell client to proceed
|
|
183
|
+
socket.write('+ Ready for literal data\r\n');
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
cmdPrefix = '';
|
|
189
|
+
dispatch(combined);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
socket.on('data', chunk => {
|
|
194
|
+
buf = Buffer.concat([buf, chunk]);
|
|
195
|
+
processBuffer();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return server;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve(server.address().port)));
|
|
203
|
+
|
|
204
|
+
const makeClient = (port, overrides = {}) =>
|
|
205
|
+
new ImapFlow({
|
|
206
|
+
host: '127.0.0.1',
|
|
207
|
+
port,
|
|
208
|
+
secure: false,
|
|
209
|
+
disableAutoIdle: true,
|
|
210
|
+
disableCompression: true,
|
|
211
|
+
logger: false,
|
|
212
|
+
auth: { user: 'test', pass: 'secret' },
|
|
213
|
+
...overrides
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Tests
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
module.exports['Server: full connect + session + logout'] = async test => {
|
|
221
|
+
let server = createServer();
|
|
222
|
+
let port = await listen(server);
|
|
223
|
+
let client = makeClient(port);
|
|
224
|
+
client.on('error', () => {});
|
|
225
|
+
|
|
226
|
+
await client.connect();
|
|
227
|
+
test.ok(client.authenticated, 'authenticated');
|
|
228
|
+
test.ok(client.usable, 'usable');
|
|
229
|
+
test.ok(client.capabilities.has('IMAP4rev1'));
|
|
230
|
+
test.ok(client.serverInfo || client.namespace, 'namespace set');
|
|
231
|
+
|
|
232
|
+
await client.logout();
|
|
233
|
+
test.equal(client.state, client.states.LOGOUT);
|
|
234
|
+
|
|
235
|
+
client.close();
|
|
236
|
+
server.close();
|
|
237
|
+
test.done();
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
module.exports['Server: qresync option adds QRESYNC to ENABLE'] = async test => {
|
|
241
|
+
let enabledArgs = null;
|
|
242
|
+
let server = createServer({
|
|
243
|
+
capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE CONDSTORE QRESYNC',
|
|
244
|
+
handlers: {
|
|
245
|
+
ENABLE(ctx) {
|
|
246
|
+
enabledArgs = ctx.args;
|
|
247
|
+
ctx.write('* ENABLED CONDSTORE QRESYNC\r\n');
|
|
248
|
+
ctx.ok('ENABLE completed');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
let port = await listen(server);
|
|
253
|
+
let client = makeClient(port, { qresync: true });
|
|
254
|
+
client.on('error', () => {});
|
|
255
|
+
|
|
256
|
+
await client.connect();
|
|
257
|
+
test.ok(/QRESYNC/.test(enabledArgs || ''), 'QRESYNC requested in ENABLE');
|
|
258
|
+
|
|
259
|
+
await client.logout();
|
|
260
|
+
client.close();
|
|
261
|
+
server.close();
|
|
262
|
+
test.done();
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
module.exports['Server: connect runs NOOP and LIST'] = async test => {
|
|
266
|
+
let server = createServer();
|
|
267
|
+
let port = await listen(server);
|
|
268
|
+
let client = makeClient(port);
|
|
269
|
+
client.on('error', () => {});
|
|
270
|
+
|
|
271
|
+
await client.connect();
|
|
272
|
+
await client.noop();
|
|
273
|
+
let folders = await client.list();
|
|
274
|
+
test.ok(folders.length >= 2);
|
|
275
|
+
|
|
276
|
+
await client.logout();
|
|
277
|
+
client.close();
|
|
278
|
+
server.close();
|
|
279
|
+
test.done();
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
module.exports['Server: enable compression negotiation (server declines)'] = async test => {
|
|
283
|
+
let server = createServer();
|
|
284
|
+
let port = await listen(server);
|
|
285
|
+
// disableCompression false -> client issues COMPRESS, server says NO
|
|
286
|
+
let client = makeClient(port, { disableCompression: false });
|
|
287
|
+
client.on('error', () => {});
|
|
288
|
+
|
|
289
|
+
await client.connect();
|
|
290
|
+
test.ok(client.usable);
|
|
291
|
+
test.ok(!client._deflate, 'compression not enabled when server declines');
|
|
292
|
+
|
|
293
|
+
await client.logout();
|
|
294
|
+
client.close();
|
|
295
|
+
server.close();
|
|
296
|
+
test.done();
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
module.exports['Server: AUTHENTICATE LOGIN flow'] = async test => {
|
|
300
|
+
let server = createServer({
|
|
301
|
+
capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE AUTH=LOGIN',
|
|
302
|
+
handlers: {
|
|
303
|
+
AUTHENTICATE(ctx) {
|
|
304
|
+
// SASL LOGIN: server prompts for username then password
|
|
305
|
+
ctx.write('+ VXNlcm5hbWU6\r\n'); // "Username:"
|
|
306
|
+
ctx.socket.once('data', () => {
|
|
307
|
+
ctx.write('+ UGFzc3dvcmQ6\r\n'); // "Password:"
|
|
308
|
+
ctx.socket.once('data', () => {
|
|
309
|
+
ctx.ok('AUTHENTICATE completed');
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
let port = await listen(server);
|
|
316
|
+
let client = makeClient(port);
|
|
317
|
+
client.on('error', () => {});
|
|
318
|
+
|
|
319
|
+
await client.connect();
|
|
320
|
+
test.ok(client.authenticated);
|
|
321
|
+
|
|
322
|
+
await client.logout();
|
|
323
|
+
client.close();
|
|
324
|
+
server.close();
|
|
325
|
+
test.done();
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
module.exports['Server: SELECT, SEARCH, STORE and EXPUNGE via run'] = async test => {
|
|
329
|
+
let server = createServer({
|
|
330
|
+
handlers: {
|
|
331
|
+
STORE(ctx) {
|
|
332
|
+
ctx.write('* 1 FETCH (FLAGS (\\Seen))\r\n');
|
|
333
|
+
ctx.ok('STORE completed');
|
|
334
|
+
},
|
|
335
|
+
EXPUNGE(ctx) {
|
|
336
|
+
ctx.write('* 1 EXPUNGE\r\n');
|
|
337
|
+
ctx.ok('EXPUNGE completed');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
let port = await listen(server);
|
|
342
|
+
let client = makeClient(port);
|
|
343
|
+
client.on('error', () => {});
|
|
344
|
+
|
|
345
|
+
await client.connect();
|
|
346
|
+
let mailbox = await client.mailboxOpen('INBOX');
|
|
347
|
+
test.equal(mailbox.exists, 3);
|
|
348
|
+
|
|
349
|
+
let found = await client.search({ seen: true }, { uid: false });
|
|
350
|
+
test.deepEqual(found, [1, 2, 3]);
|
|
351
|
+
|
|
352
|
+
let stored = await client.messageFlagsAdd('1', ['\\Seen']);
|
|
353
|
+
test.ok(stored);
|
|
354
|
+
|
|
355
|
+
let expungeEvents = [];
|
|
356
|
+
client.on('expunge', e => expungeEvents.push(e));
|
|
357
|
+
await client.messageDelete('1', { uid: false });
|
|
358
|
+
test.ok(expungeEvents.length >= 1);
|
|
359
|
+
|
|
360
|
+
await client.logout();
|
|
361
|
+
client.close();
|
|
362
|
+
server.close();
|
|
363
|
+
test.done();
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
module.exports['Server: FETCH via fetchOne'] = async test => {
|
|
367
|
+
let server = createServer({
|
|
368
|
+
handlers: {
|
|
369
|
+
FETCH(ctx) {
|
|
370
|
+
ctx.write('* 1 FETCH (UID 11 FLAGS (\\Seen) RFC822.SIZE 42)\r\n');
|
|
371
|
+
ctx.ok('FETCH completed');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
let port = await listen(server);
|
|
376
|
+
let client = makeClient(port);
|
|
377
|
+
client.on('error', () => {});
|
|
378
|
+
|
|
379
|
+
await client.connect();
|
|
380
|
+
await client.mailboxOpen('INBOX');
|
|
381
|
+
let msg = await client.fetchOne('1', { uid: true, flags: true, size: true });
|
|
382
|
+
test.equal(msg.uid, 11);
|
|
383
|
+
test.equal(msg.size, 42);
|
|
384
|
+
test.ok(msg.flags.has('\\Seen'));
|
|
385
|
+
|
|
386
|
+
await client.logout();
|
|
387
|
+
client.close();
|
|
388
|
+
server.close();
|
|
389
|
+
test.done();
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
module.exports['Server: APPEND with synchronizing literal'] = async test => {
|
|
393
|
+
let server = createServer({
|
|
394
|
+
capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE UIDPLUS',
|
|
395
|
+
handlers: {
|
|
396
|
+
APPEND(ctx) {
|
|
397
|
+
ctx.ok('[APPENDUID 12345 9] APPEND completed');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
let port = await listen(server);
|
|
402
|
+
let client = makeClient(port);
|
|
403
|
+
client.on('error', () => {});
|
|
404
|
+
|
|
405
|
+
await client.connect();
|
|
406
|
+
let res = await client.append('INBOX', 'Subject: hi\r\n\r\nbody', ['\\Seen']);
|
|
407
|
+
test.ok(res);
|
|
408
|
+
test.equal(res.uid, 9);
|
|
409
|
+
|
|
410
|
+
await client.logout();
|
|
411
|
+
client.close();
|
|
412
|
+
server.close();
|
|
413
|
+
test.done();
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
module.exports['Server: AUTHENTICATE PLAIN flow'] = async test => {
|
|
417
|
+
let server = createServer({
|
|
418
|
+
capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE AUTH=PLAIN',
|
|
419
|
+
handlers: {
|
|
420
|
+
AUTHENTICATE(ctx) {
|
|
421
|
+
ctx.write('+ \r\n');
|
|
422
|
+
ctx.socket.once('data', () => ctx.ok('AUTHENTICATE completed'));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
let port = await listen(server);
|
|
427
|
+
let client = makeClient(port);
|
|
428
|
+
client.on('error', () => {});
|
|
429
|
+
|
|
430
|
+
await client.connect();
|
|
431
|
+
test.ok(client.authenticated);
|
|
432
|
+
|
|
433
|
+
await client.logout();
|
|
434
|
+
client.close();
|
|
435
|
+
server.close();
|
|
436
|
+
test.done();
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
module.exports['Server: AUTHENTICATE XOAUTH2 with accessToken'] = async test => {
|
|
440
|
+
let server = createServer({
|
|
441
|
+
capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE AUTH=XOAUTH2',
|
|
442
|
+
handlers: {
|
|
443
|
+
AUTHENTICATE(ctx) {
|
|
444
|
+
ctx.ok('AUTHENTICATE completed');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
let port = await listen(server);
|
|
449
|
+
let client = makeClient(port, { auth: { user: 'test', accessToken: 'token-123' } });
|
|
450
|
+
client.on('error', () => {});
|
|
451
|
+
|
|
452
|
+
await client.connect();
|
|
453
|
+
test.ok(client.authenticated);
|
|
454
|
+
|
|
455
|
+
await client.logout();
|
|
456
|
+
client.close();
|
|
457
|
+
server.close();
|
|
458
|
+
test.done();
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
module.exports['Server: backslash username forces LOGIN method'] = async test => {
|
|
462
|
+
let loginUsed = false;
|
|
463
|
+
let server = createServer({
|
|
464
|
+
capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE AUTH=PLAIN',
|
|
465
|
+
handlers: {
|
|
466
|
+
LOGIN(ctx) {
|
|
467
|
+
loginUsed = true;
|
|
468
|
+
ctx.ok('LOGIN completed');
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
let port = await listen(server);
|
|
473
|
+
let client = makeClient(port, { auth: { user: 'domain\\user', pass: 'secret' } });
|
|
474
|
+
client.on('error', () => {});
|
|
475
|
+
|
|
476
|
+
await client.connect();
|
|
477
|
+
test.ok(client.authenticated);
|
|
478
|
+
test.ok(loginUsed, 'used LOGIN command despite AUTH=PLAIN');
|
|
479
|
+
|
|
480
|
+
await client.logout();
|
|
481
|
+
client.close();
|
|
482
|
+
server.close();
|
|
483
|
+
test.done();
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
module.exports['Server: LOGINDISABLED rejects connect'] = async test => {
|
|
487
|
+
let server = createServer({
|
|
488
|
+
capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE LOGINDISABLED'
|
|
489
|
+
});
|
|
490
|
+
let port = await listen(server);
|
|
491
|
+
let client = makeClient(port);
|
|
492
|
+
client.on('error', () => {});
|
|
493
|
+
|
|
494
|
+
let err = null;
|
|
495
|
+
try {
|
|
496
|
+
await client.connect();
|
|
497
|
+
} catch (e) {
|
|
498
|
+
err = e;
|
|
499
|
+
}
|
|
500
|
+
test.ok(err, 'connect rejected when login disabled');
|
|
501
|
+
|
|
502
|
+
client.close();
|
|
503
|
+
server.close();
|
|
504
|
+
test.done();
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
module.exports['Server: missing auth config rejects connect'] = async test => {
|
|
508
|
+
let server = createServer();
|
|
509
|
+
let port = await listen(server);
|
|
510
|
+
let client = makeClient(port, { auth: false });
|
|
511
|
+
client.on('error', () => {});
|
|
512
|
+
|
|
513
|
+
let err = null;
|
|
514
|
+
try {
|
|
515
|
+
await client.connect();
|
|
516
|
+
} catch (e) {
|
|
517
|
+
err = e;
|
|
518
|
+
}
|
|
519
|
+
test.ok(err, 'connect rejected without auth');
|
|
520
|
+
|
|
521
|
+
client.close();
|
|
522
|
+
server.close();
|
|
523
|
+
test.done();
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
module.exports['Server: auth without password rejects connect'] = async test => {
|
|
527
|
+
let server = createServer();
|
|
528
|
+
let port = await listen(server);
|
|
529
|
+
let client = makeClient(port, { auth: { user: 'only-user' } });
|
|
530
|
+
client.on('error', () => {});
|
|
531
|
+
|
|
532
|
+
let err = null;
|
|
533
|
+
try {
|
|
534
|
+
await client.connect();
|
|
535
|
+
} catch (e) {
|
|
536
|
+
err = e;
|
|
537
|
+
}
|
|
538
|
+
test.ok(err, 'connect rejected without password');
|
|
539
|
+
|
|
540
|
+
client.close();
|
|
541
|
+
server.close();
|
|
542
|
+
test.done();
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
module.exports['Server: command returning NO rejects with responseStatus'] = async test => {
|
|
546
|
+
let server = createServer({
|
|
547
|
+
handlers: {
|
|
548
|
+
CREATE(ctx) {
|
|
549
|
+
ctx.no('Mailbox already exists');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
let port = await listen(server);
|
|
554
|
+
let client = makeClient(port);
|
|
555
|
+
client.on('error', () => {});
|
|
556
|
+
|
|
557
|
+
await client.connect();
|
|
558
|
+
let err = null;
|
|
559
|
+
try {
|
|
560
|
+
await client.mailboxCreate('Existing');
|
|
561
|
+
} catch (e) {
|
|
562
|
+
err = e;
|
|
563
|
+
}
|
|
564
|
+
test.ok(err);
|
|
565
|
+
test.equal(err.responseStatus, 'NO');
|
|
566
|
+
|
|
567
|
+
await client.logout();
|
|
568
|
+
client.close();
|
|
569
|
+
server.close();
|
|
570
|
+
test.done();
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
module.exports['Server: command returning BAD rejects'] = async test => {
|
|
574
|
+
let server = createServer({
|
|
575
|
+
handlers: {
|
|
576
|
+
CREATE(ctx) {
|
|
577
|
+
ctx.bad('Invalid arguments');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
let port = await listen(server);
|
|
582
|
+
let client = makeClient(port);
|
|
583
|
+
client.on('error', () => {});
|
|
584
|
+
|
|
585
|
+
await client.connect();
|
|
586
|
+
let err = null;
|
|
587
|
+
try {
|
|
588
|
+
await client.mailboxCreate('Whatever');
|
|
589
|
+
} catch (e) {
|
|
590
|
+
err = e;
|
|
591
|
+
}
|
|
592
|
+
test.ok(err);
|
|
593
|
+
test.equal(err.responseStatus, 'BAD');
|
|
594
|
+
|
|
595
|
+
await client.logout();
|
|
596
|
+
client.close();
|
|
597
|
+
server.close();
|
|
598
|
+
test.done();
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
module.exports['Server: verifyOnly connect lists mailboxes and logs out'] = async test => {
|
|
602
|
+
let server = createServer();
|
|
603
|
+
let port = await listen(server);
|
|
604
|
+
let client = makeClient(port, { verifyOnly: true, includeMailboxes: true });
|
|
605
|
+
client.on('error', () => {});
|
|
606
|
+
|
|
607
|
+
await client.connect();
|
|
608
|
+
test.ok(Array.isArray(client._mailboxList), 'mailbox list captured');
|
|
609
|
+
test.ok(client._mailboxList.length >= 1);
|
|
610
|
+
// verifyOnly logs out at the end of startSession
|
|
611
|
+
test.equal(client.state, client.states.LOGOUT);
|
|
612
|
+
|
|
613
|
+
client.close();
|
|
614
|
+
server.close();
|
|
615
|
+
test.done();
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
module.exports['Server: ID is re-requested after login when first response is sparse'] = async test => {
|
|
619
|
+
let idCalls = 0;
|
|
620
|
+
let server = createServer({
|
|
621
|
+
handlers: {
|
|
622
|
+
ID(ctx) {
|
|
623
|
+
idCalls++;
|
|
624
|
+
if (idCalls === 1) {
|
|
625
|
+
// sparse/NIL ID before login triggers a re-request afterwards
|
|
626
|
+
ctx.write('* ID NIL\r\n');
|
|
627
|
+
} else {
|
|
628
|
+
ctx.write('* ID ("name" "mock" "version" "1")\r\n');
|
|
629
|
+
}
|
|
630
|
+
ctx.ok('ID completed');
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
let port = await listen(server);
|
|
635
|
+
let client = makeClient(port);
|
|
636
|
+
client.on('error', () => {});
|
|
637
|
+
|
|
638
|
+
await client.connect();
|
|
639
|
+
test.ok(idCalls >= 2, 'ID requested again after login');
|
|
640
|
+
|
|
641
|
+
await client.logout();
|
|
642
|
+
client.close();
|
|
643
|
+
server.close();
|
|
644
|
+
test.done();
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
module.exports['Server: NAMESPACE BAD with auth message surfaces auth failure'] = async test => {
|
|
648
|
+
let server = createServer({
|
|
649
|
+
handlers: {
|
|
650
|
+
NAMESPACE(ctx) {
|
|
651
|
+
ctx.bad('User is authenticated but not connected');
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
let port = await listen(server);
|
|
656
|
+
let client = makeClient(port);
|
|
657
|
+
client.on('error', () => {});
|
|
658
|
+
|
|
659
|
+
let err = null;
|
|
660
|
+
try {
|
|
661
|
+
await client.connect();
|
|
662
|
+
} catch (e) {
|
|
663
|
+
err = e;
|
|
664
|
+
}
|
|
665
|
+
test.ok(err, 'connect rejected');
|
|
666
|
+
test.ok(/Authentication failed/i.test(err.message) || err.authenticationFailed, 'reported as auth failure');
|
|
667
|
+
|
|
668
|
+
client.close();
|
|
669
|
+
server.close();
|
|
670
|
+
test.done();
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
module.exports['Server: getMailboxLock selects and releases'] = async test => {
|
|
674
|
+
let server = createServer();
|
|
675
|
+
let port = await listen(server);
|
|
676
|
+
let client = makeClient(port);
|
|
677
|
+
client.on('error', () => {});
|
|
678
|
+
|
|
679
|
+
await client.connect();
|
|
680
|
+
let lock = await client.getMailboxLock('INBOX');
|
|
681
|
+
test.equal(client.mailbox.path, 'INBOX');
|
|
682
|
+
lock.release();
|
|
683
|
+
|
|
684
|
+
// second lock on same mailbox -> fast path
|
|
685
|
+
let lock2 = await client.getMailboxLock('INBOX');
|
|
686
|
+
lock2.release();
|
|
687
|
+
|
|
688
|
+
await client.logout();
|
|
689
|
+
client.close();
|
|
690
|
+
server.close();
|
|
691
|
+
test.done();
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
module.exports['Server: getMailboxLock rejects for missing mailbox'] = async test => {
|
|
695
|
+
let server = createServer({
|
|
696
|
+
handlers: {
|
|
697
|
+
SELECT(ctx) {
|
|
698
|
+
ctx.no('Mailbox does not exist');
|
|
699
|
+
},
|
|
700
|
+
LIST(ctx) {
|
|
701
|
+
// empty LIST -> mailbox confirmed missing
|
|
702
|
+
ctx.ok('LIST completed');
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
let port = await listen(server);
|
|
707
|
+
let client = makeClient(port);
|
|
708
|
+
client.on('error', () => {});
|
|
709
|
+
|
|
710
|
+
await client.connect();
|
|
711
|
+
let err = null;
|
|
712
|
+
try {
|
|
713
|
+
await client.getMailboxLock('Missing');
|
|
714
|
+
} catch (e) {
|
|
715
|
+
err = e;
|
|
716
|
+
}
|
|
717
|
+
test.ok(err);
|
|
718
|
+
test.ok(err.mailboxMissing, 'flagged as missing mailbox');
|
|
719
|
+
|
|
720
|
+
client.close();
|
|
721
|
+
server.close();
|
|
722
|
+
test.done();
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
module.exports['Server: IDLE then break out'] = async test => {
|
|
726
|
+
test.expect(2);
|
|
727
|
+
let idleTag = null;
|
|
728
|
+
let doneReceived = false;
|
|
729
|
+
// Capabilities MUST advertise IDLE, otherwise the client falls back to NOOP polling
|
|
730
|
+
// and never sends an IDLE command at all.
|
|
731
|
+
let server = createServer({
|
|
732
|
+
capabilities: 'IMAP4rev1 IDLE ID ENABLE NAMESPACE UIDPLUS CONDSTORE MOVE QUOTA',
|
|
733
|
+
handlers: {
|
|
734
|
+
IDLE(ctx) {
|
|
735
|
+
// Remember the IDLE command tag so the matching DONE can complete it.
|
|
736
|
+
idleTag = ctx.tag;
|
|
737
|
+
ctx.write('+ idling\r\n');
|
|
738
|
+
},
|
|
739
|
+
DONE(ctx) {
|
|
740
|
+
doneReceived = true;
|
|
741
|
+
// Complete the original IDLE command so client.idle() resolves cleanly.
|
|
742
|
+
ctx.socket.write(`${idleTag} OK IDLE terminated\r\n`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
let port = await listen(server);
|
|
747
|
+
let client = makeClient(port);
|
|
748
|
+
client.on('error', () => {});
|
|
749
|
+
|
|
750
|
+
await client.connect();
|
|
751
|
+
await client.mailboxOpen('INBOX');
|
|
752
|
+
|
|
753
|
+
// Enter IDLE, then break out by issuing another command. Queuing NOOP triggers
|
|
754
|
+
// preCheck(), which sends DONE; the server's DONE handler then completes IDLE.
|
|
755
|
+
let idlePromise = client.idle();
|
|
756
|
+
await client.noop();
|
|
757
|
+
await idlePromise;
|
|
758
|
+
|
|
759
|
+
test.ok(doneReceived, 'server received the DONE continuation');
|
|
760
|
+
test.equal(client.idling, false, 'client left the IDLE state');
|
|
761
|
+
|
|
762
|
+
client.close();
|
|
763
|
+
server.close();
|
|
764
|
+
test.done();
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
module.exports['Server: stats counts bytes'] = async test => {
|
|
768
|
+
let server = createServer();
|
|
769
|
+
let port = await listen(server);
|
|
770
|
+
let client = makeClient(port);
|
|
771
|
+
client.on('error', () => {});
|
|
772
|
+
|
|
773
|
+
await client.connect();
|
|
774
|
+
let stats = client.stats();
|
|
775
|
+
test.ok(stats.sent > 0);
|
|
776
|
+
test.ok(stats.received > 0);
|
|
777
|
+
|
|
778
|
+
let stats2 = client.stats(true); // reset
|
|
779
|
+
test.ok(stats2.sent >= 0);
|
|
780
|
+
let stats3 = client.stats();
|
|
781
|
+
test.equal(stats3.sent, 0);
|
|
782
|
+
|
|
783
|
+
await client.logout();
|
|
784
|
+
client.close();
|
|
785
|
+
server.close();
|
|
786
|
+
test.done();
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
module.exports['Server: PREAUTH greeting skips login'] = async test => {
|
|
790
|
+
let server = createServer({
|
|
791
|
+
greeting: '* PREAUTH [CAPABILITY IMAP4rev1 ID ENABLE NAMESPACE] already authenticated\r\n'
|
|
792
|
+
});
|
|
793
|
+
let port = await listen(server);
|
|
794
|
+
let client = makeClient(port);
|
|
795
|
+
client.on('error', () => {});
|
|
796
|
+
|
|
797
|
+
await client.connect();
|
|
798
|
+
test.equal(client.state, client.states.AUTHENTICATED);
|
|
799
|
+
test.ok(client.usable);
|
|
800
|
+
|
|
801
|
+
await client.logout();
|
|
802
|
+
client.close();
|
|
803
|
+
server.close();
|
|
804
|
+
test.done();
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
module.exports['Server: unsolicited EXISTS and VANISHED reach the untagged handlers'] = async test => {
|
|
808
|
+
let server = createServer({
|
|
809
|
+
capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE QRESYNC',
|
|
810
|
+
handlers: {
|
|
811
|
+
NOOP(ctx) {
|
|
812
|
+
// unsolicited untagged updates piggybacked on NOOP
|
|
813
|
+
ctx.write('* 5 EXISTS\r\n');
|
|
814
|
+
ctx.write('* VANISHED 1:2\r\n');
|
|
815
|
+
ctx.ok('NOOP completed');
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
let port = await listen(server);
|
|
820
|
+
let client = makeClient(port);
|
|
821
|
+
client.on('error', () => {});
|
|
822
|
+
|
|
823
|
+
await client.connect();
|
|
824
|
+
await client.mailboxOpen('INBOX');
|
|
825
|
+
|
|
826
|
+
let existsEvent = null;
|
|
827
|
+
let expungeEvents = [];
|
|
828
|
+
client.on('exists', e => {
|
|
829
|
+
existsEvent = e;
|
|
830
|
+
});
|
|
831
|
+
client.on('expunge', e => expungeEvents.push(e));
|
|
832
|
+
|
|
833
|
+
await client.noop();
|
|
834
|
+
// allow the untagged handlers to run
|
|
835
|
+
await new Promise(r => setTimeout(r, 20));
|
|
836
|
+
|
|
837
|
+
test.ok(existsEvent, 'EXISTS handler fired');
|
|
838
|
+
test.equal(existsEvent.count, 5);
|
|
839
|
+
test.ok(expungeEvents.length >= 1, 'VANISHED handler fired');
|
|
840
|
+
test.equal(expungeEvents[0].vanished, true);
|
|
841
|
+
|
|
842
|
+
await client.logout();
|
|
843
|
+
client.close();
|
|
844
|
+
server.close();
|
|
845
|
+
test.done();
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
module.exports['Server: invalid tagged response rejects with InvalidResponse'] = async test => {
|
|
849
|
+
let server = createServer({
|
|
850
|
+
handlers: {
|
|
851
|
+
CREATE(ctx) {
|
|
852
|
+
// a tagged response whose command is neither OK/NO/BAD
|
|
853
|
+
ctx.write(`${ctx.tag} WEIRD unexpected status\r\n`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
let port = await listen(server);
|
|
858
|
+
let client = makeClient(port);
|
|
859
|
+
client.on('error', () => {});
|
|
860
|
+
|
|
861
|
+
await client.connect();
|
|
862
|
+
let err = null;
|
|
863
|
+
try {
|
|
864
|
+
await client.mailboxCreate('Whatever');
|
|
865
|
+
} catch (e) {
|
|
866
|
+
err = e;
|
|
867
|
+
}
|
|
868
|
+
test.ok(err);
|
|
869
|
+
test.equal(err.code, 'InvalidResponse');
|
|
870
|
+
|
|
871
|
+
client.close();
|
|
872
|
+
server.close();
|
|
873
|
+
test.done();
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
module.exports['Server: reader tolerates an unparseable untagged line'] = async test => {
|
|
877
|
+
let server = createServer({
|
|
878
|
+
handlers: {
|
|
879
|
+
NOOP(ctx) {
|
|
880
|
+
// malformed untagged line that the parser cannot parse, then a valid OK
|
|
881
|
+
ctx.write('* 1 FETCH (]\r\n');
|
|
882
|
+
ctx.ok('NOOP completed');
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
let port = await listen(server);
|
|
887
|
+
let client = makeClient(port);
|
|
888
|
+
client.on('error', () => {});
|
|
889
|
+
|
|
890
|
+
await client.connect();
|
|
891
|
+
// The malformed line is logged & skipped; NOOP still resolves
|
|
892
|
+
await client.noop();
|
|
893
|
+
test.ok(true);
|
|
894
|
+
|
|
895
|
+
await client.logout();
|
|
896
|
+
client.close();
|
|
897
|
+
server.close();
|
|
898
|
+
test.done();
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
module.exports['Server: reader yields to event loop on many untagged responses'] = async test => {
|
|
902
|
+
let server = createServer({
|
|
903
|
+
handlers: {
|
|
904
|
+
STORE(ctx) {
|
|
905
|
+
// 12 untagged FETCH responses + OK in a SINGLE write. STORE's FETCH
|
|
906
|
+
// responses go through the (non-backpressured) global handler, so the
|
|
907
|
+
// reader drains all >10 items in one pass and hits the periodic yield.
|
|
908
|
+
let out = '';
|
|
909
|
+
for (let i = 1; i <= 12; i++) {
|
|
910
|
+
out += `* ${i} FETCH (FLAGS (\\Seen))\r\n`;
|
|
911
|
+
}
|
|
912
|
+
out += `${ctx.tag} OK STORE completed\r\n`;
|
|
913
|
+
ctx.write(out);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
let port = await listen(server);
|
|
918
|
+
let client = makeClient(port);
|
|
919
|
+
client.on('error', () => {});
|
|
920
|
+
|
|
921
|
+
await client.connect();
|
|
922
|
+
await client.mailboxOpen('INBOX');
|
|
923
|
+
let ok = await client.messageFlagsAdd('1:12', ['\\Seen']);
|
|
924
|
+
test.ok(ok);
|
|
925
|
+
|
|
926
|
+
await client.logout();
|
|
927
|
+
client.close();
|
|
928
|
+
server.close();
|
|
929
|
+
test.done();
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
module.exports['Server: partial FETCH NO is treated as success'] = async test => {
|
|
933
|
+
let server = createServer({
|
|
934
|
+
handlers: {
|
|
935
|
+
FETCH(ctx) {
|
|
936
|
+
ctx.write('* 1 FETCH (UID 1 FLAGS (\\Seen))\r\n');
|
|
937
|
+
ctx.no('Some of the requested messages no longer exist');
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
let port = await listen(server);
|
|
942
|
+
let client = makeClient(port);
|
|
943
|
+
client.on('error', () => {});
|
|
944
|
+
|
|
945
|
+
await client.connect();
|
|
946
|
+
await client.mailboxOpen('INBOX');
|
|
947
|
+
// The NO with this specific text is treated as success rather than rejecting
|
|
948
|
+
let msg = await client.fetchOne('1', { uid: true });
|
|
949
|
+
test.ok(msg);
|
|
950
|
+
test.equal(msg.uid, 1);
|
|
951
|
+
|
|
952
|
+
await client.logout();
|
|
953
|
+
client.close();
|
|
954
|
+
server.close();
|
|
955
|
+
test.done();
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
// NB: the socket-timeout -> NOOP/IDLE recovery handler is covered deterministically
|
|
959
|
+
// in imap-flow-coverage-test.js (the _socketTimeout tests) rather than via a flaky
|
|
960
|
+
// real-clock socket timeout here.
|
|
961
|
+
|
|
962
|
+
module.exports['Server: greeting timeout rejects connect'] = async test => {
|
|
963
|
+
// Server accepts the socket but never sends a greeting
|
|
964
|
+
let server = net.createServer(socket => {
|
|
965
|
+
socket.on('error', () => {});
|
|
966
|
+
// intentionally silent
|
|
967
|
+
});
|
|
968
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
969
|
+
let port = server.address().port;
|
|
970
|
+
|
|
971
|
+
let client = makeClient(port, { greetingTimeout: 100 });
|
|
972
|
+
client.on('error', () => {});
|
|
973
|
+
|
|
974
|
+
let err = null;
|
|
975
|
+
try {
|
|
976
|
+
await client.connect();
|
|
977
|
+
} catch (e) {
|
|
978
|
+
err = e;
|
|
979
|
+
}
|
|
980
|
+
test.ok(err);
|
|
981
|
+
test.equal(err.code, 'GREETING_TIMEOUT');
|
|
982
|
+
|
|
983
|
+
client.close();
|
|
984
|
+
server.close();
|
|
985
|
+
test.done();
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
module.exports['Server: connection timeout rejects connect'] = async test => {
|
|
989
|
+
// 192.0.2.0/24 (TEST-NET-1, RFC 5737) is reserved and never answers, so the TCP
|
|
990
|
+
// connect hangs until the (very short) connection timeout fires.
|
|
991
|
+
let client = makeClient(9, { host: '192.0.2.1', connectionTimeout: 120 });
|
|
992
|
+
client.on('error', () => {});
|
|
993
|
+
|
|
994
|
+
let err = null;
|
|
995
|
+
try {
|
|
996
|
+
await client.connect();
|
|
997
|
+
} catch (e) {
|
|
998
|
+
err = e;
|
|
999
|
+
}
|
|
1000
|
+
test.ok(err, 'connect rejected');
|
|
1001
|
+
test.equal(err.code, 'CONNECT_TIMEOUT');
|
|
1002
|
+
|
|
1003
|
+
client.close();
|
|
1004
|
+
test.done();
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
module.exports['Server: proxy connection failure rejects connect'] = async test => {
|
|
1008
|
+
let client = makeClient(1, {
|
|
1009
|
+
// point at a port with nothing listening so the proxy setup fails
|
|
1010
|
+
proxy: 'socks://127.0.0.1:1'
|
|
1011
|
+
});
|
|
1012
|
+
client.on('error', () => {});
|
|
1013
|
+
|
|
1014
|
+
let err = null;
|
|
1015
|
+
try {
|
|
1016
|
+
await client.connect();
|
|
1017
|
+
} catch (e) {
|
|
1018
|
+
err = e;
|
|
1019
|
+
}
|
|
1020
|
+
test.ok(err, 'connect rejected on proxy failure');
|
|
1021
|
+
|
|
1022
|
+
client.close();
|
|
1023
|
+
test.done();
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
module.exports['Server: socket close triggers close handling'] = async test => {
|
|
1027
|
+
let server = createServer();
|
|
1028
|
+
let port = await listen(server);
|
|
1029
|
+
let client = makeClient(port);
|
|
1030
|
+
client.on('error', () => {});
|
|
1031
|
+
|
|
1032
|
+
await client.connect();
|
|
1033
|
+
let closed = false;
|
|
1034
|
+
client.on('close', () => {
|
|
1035
|
+
closed = true;
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
// Drop the server side
|
|
1039
|
+
server.close();
|
|
1040
|
+
client.socket.destroy();
|
|
1041
|
+
|
|
1042
|
+
await new Promise(r => setTimeout(r, 60));
|
|
1043
|
+
test.ok(closed || client.isClosed, 'connection closed');
|
|
1044
|
+
client.close();
|
|
1045
|
+
test.done();
|
|
1046
|
+
};
|