imapflow 1.2.13 → 1.2.15
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 +15 -0
- package/lib/charsets.js +1 -4
- package/lib/commands/append.js +1 -1
- package/lib/commands/copy.js +3 -24
- package/lib/commands/copyuid-parser.js +33 -0
- package/lib/commands/idle.js +5 -7
- package/lib/commands/list.js +1 -1
- package/lib/commands/move.js +4 -27
- package/lib/commands/store.js +1 -3
- package/lib/handler/imap-compiler.js +1 -3
- package/lib/handler/imap-formal-syntax.js +1 -7
- package/lib/handler/imap-parser.js +1 -1
- package/lib/handler/imap-stream.js +7 -13
- package/lib/handler/parser-instance.js +2 -2
- package/lib/handler/token-parser.js +9 -9
- package/lib/imap-flow.js +100 -141
- package/lib/search-compiler.js +4 -10
- package/lib/tools.js +29 -36
- package/package.json +2 -2
- package/test/charsets-test.js +161 -0
- package/test/commands-integration-test.js +61 -0
- package/test/connection-edge-cases-test.js +102 -16
- package/test/copyuid-parser-test.js +153 -0
- package/test/imap-compiler-test.js +66 -0
- package/test/imap-formal-syntax-test.js +275 -0
- package/test/imap-stream-edge-cases-test.js +268 -0
- package/test/imapflow-test.js +212 -0
- package/test/search-compiler-test.js +74 -0
- package/test/token-parser-test.js +374 -0
- package/test/tools-test.js +149 -2
- package/test/unhandled-rejection-test.js +150 -11
package/test/tools-test.js
CHANGED
|
@@ -270,6 +270,13 @@ module.exports['Tools: getFlagColor with color bits'] = test => {
|
|
|
270
270
|
test.done();
|
|
271
271
|
};
|
|
272
272
|
|
|
273
|
+
module.exports['Tools: getFlagColor with all bits set (index 7) defaults to red'] = test => {
|
|
274
|
+
// bit0=1, bit1=2, bit2=4 => color=7, FLAG_COLORS[7] is undefined => defaults to 'red'
|
|
275
|
+
let flags = new Set(['\\Flagged', '$MailFlagBit0', '$MailFlagBit1', '$MailFlagBit2']);
|
|
276
|
+
test.equal(tools.getFlagColor(flags), 'red');
|
|
277
|
+
test.done();
|
|
278
|
+
};
|
|
279
|
+
|
|
273
280
|
// ============================================
|
|
274
281
|
// getColorFlags tests
|
|
275
282
|
// ============================================
|
|
@@ -284,11 +291,11 @@ module.exports['Tools: getColorFlags with valid color'] = test => {
|
|
|
284
291
|
};
|
|
285
292
|
|
|
286
293
|
module.exports['Tools: getColorFlags with red (index 0)'] = test => {
|
|
287
|
-
// 'red' is index 0
|
|
294
|
+
// 'red' is index 0 — should add \\Flagged and remove all MailFlagBit flags
|
|
288
295
|
let result = tools.getColorFlags('red');
|
|
289
296
|
test.ok(Array.isArray(result.add));
|
|
290
297
|
test.ok(Array.isArray(result.remove));
|
|
291
|
-
test.ok(result.
|
|
298
|
+
test.ok(result.add.includes('\\Flagged'));
|
|
292
299
|
test.done();
|
|
293
300
|
};
|
|
294
301
|
|
|
@@ -990,3 +997,143 @@ module.exports['Tools: AuthenticationFailure error class'] = test => {
|
|
|
990
997
|
test.equal(error.message, 'Auth failed');
|
|
991
998
|
test.done();
|
|
992
999
|
};
|
|
1000
|
+
|
|
1001
|
+
// ============================================
|
|
1002
|
+
// enhanceCommandError tests
|
|
1003
|
+
// ============================================
|
|
1004
|
+
|
|
1005
|
+
module.exports['Tools: enhanceCommandError sets serverResponseCode'] = async test => {
|
|
1006
|
+
let err = new Error('Command failed');
|
|
1007
|
+
err.response = {
|
|
1008
|
+
tag: '*',
|
|
1009
|
+
command: 'NO',
|
|
1010
|
+
attributes: [
|
|
1011
|
+
{
|
|
1012
|
+
type: 'SECTION',
|
|
1013
|
+
section: [{ type: 'ATOM', value: 'NONEXISTENT' }]
|
|
1014
|
+
},
|
|
1015
|
+
{ type: 'ATOM', value: 'Mailbox' },
|
|
1016
|
+
{ type: 'ATOM', value: 'not' },
|
|
1017
|
+
{ type: 'ATOM', value: 'found' }
|
|
1018
|
+
]
|
|
1019
|
+
};
|
|
1020
|
+
let result = await tools.enhanceCommandError(err);
|
|
1021
|
+
test.equal(result.serverResponseCode, 'NONEXISTENT');
|
|
1022
|
+
test.equal(typeof result.response, 'string');
|
|
1023
|
+
test.done();
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
module.exports['Tools: enhanceCommandError with no status code'] = async test => {
|
|
1027
|
+
let err = new Error('Command failed');
|
|
1028
|
+
err.response = { tag: '*', command: 'NO' };
|
|
1029
|
+
let result = await tools.enhanceCommandError(err);
|
|
1030
|
+
test.ok(!result.serverResponseCode);
|
|
1031
|
+
test.done();
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
module.exports['Tools: enhanceCommandError with null response'] = async test => {
|
|
1035
|
+
let err = new Error('Command failed');
|
|
1036
|
+
err.response = null;
|
|
1037
|
+
let result = await tools.enhanceCommandError(err);
|
|
1038
|
+
test.equal(result.response, false);
|
|
1039
|
+
test.done();
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
// ============================================
|
|
1043
|
+
// getDecoder additional tests
|
|
1044
|
+
// ============================================
|
|
1045
|
+
|
|
1046
|
+
module.exports['Tools: getDecoder with eucjp charset'] = test => {
|
|
1047
|
+
let decoder = tools.getDecoder('eucjp');
|
|
1048
|
+
test.ok(decoder);
|
|
1049
|
+
test.equal(decoder.constructor.name, 'JPDecoder');
|
|
1050
|
+
test.done();
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
module.exports['Tools: getDecoder with euc-jp (hyphenated) returns JPDecoder'] = test => {
|
|
1054
|
+
let decoder = tools.getDecoder('euc-jp');
|
|
1055
|
+
test.ok(decoder);
|
|
1056
|
+
test.equal(decoder.constructor.name, 'JPDecoder');
|
|
1057
|
+
test.done();
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
module.exports['Tools: getDecoder with jis charset'] = test => {
|
|
1061
|
+
let decoder = tools.getDecoder('jis');
|
|
1062
|
+
test.ok(decoder);
|
|
1063
|
+
test.equal(decoder.constructor.name, 'JPDecoder');
|
|
1064
|
+
test.done();
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
module.exports['Tools: getDecoder with windows-1252 returns iconv stream'] = test => {
|
|
1068
|
+
let decoder = tools.getDecoder('windows-1252');
|
|
1069
|
+
test.ok(decoder);
|
|
1070
|
+
test.notEqual(decoder.constructor.name, 'JPDecoder');
|
|
1071
|
+
test.ok(typeof decoder.write === 'function');
|
|
1072
|
+
test.done();
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
module.exports['Tools: getDecoder with no arg defaults to ascii'] = test => {
|
|
1076
|
+
let decoder = tools.getDecoder();
|
|
1077
|
+
test.ok(decoder);
|
|
1078
|
+
test.ok(typeof decoder.write === 'function');
|
|
1079
|
+
test.done();
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
// ============================================
|
|
1083
|
+
// getColorFlags tests
|
|
1084
|
+
// ============================================
|
|
1085
|
+
|
|
1086
|
+
module.exports['Tools: getColorFlags returns non-null for all valid colors'] = test => {
|
|
1087
|
+
let colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];
|
|
1088
|
+
for (let color of colors) {
|
|
1089
|
+
let result = tools.getColorFlags(color);
|
|
1090
|
+
test.ok(result, `getColorFlags('${color}') should not be null`);
|
|
1091
|
+
test.ok(Array.isArray(result.add), `${color} should have add array`);
|
|
1092
|
+
test.ok(Array.isArray(result.remove), `${color} should have remove array`);
|
|
1093
|
+
}
|
|
1094
|
+
test.done();
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
module.exports['Tools: getColorFlags red has Flagged but no MailFlagBit set'] = test => {
|
|
1098
|
+
let result = tools.getColorFlags('red');
|
|
1099
|
+
// red = index 0, all bits 0 — adds \\Flagged, removes all MailFlagBit flags
|
|
1100
|
+
test.ok(result.add.includes('\\Flagged'));
|
|
1101
|
+
test.ok(result.remove.includes('$MailFlagBit0'));
|
|
1102
|
+
test.ok(result.remove.includes('$MailFlagBit1'));
|
|
1103
|
+
test.ok(result.remove.includes('$MailFlagBit2'));
|
|
1104
|
+
test.done();
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1107
|
+
module.exports['Tools: getColorFlags orange has MailFlagBit0 set'] = test => {
|
|
1108
|
+
let result = tools.getColorFlags('orange');
|
|
1109
|
+
// orange = index 1, bit 0 set
|
|
1110
|
+
test.ok(result.add.includes('\\Flagged'));
|
|
1111
|
+
test.ok(result.add.includes('$MailFlagBit0'));
|
|
1112
|
+
test.ok(result.remove.includes('$MailFlagBit1'));
|
|
1113
|
+
test.ok(result.remove.includes('$MailFlagBit2'));
|
|
1114
|
+
test.done();
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
module.exports['Tools: getColorFlags yellow has MailFlagBit1 set'] = test => {
|
|
1118
|
+
let result = tools.getColorFlags('yellow');
|
|
1119
|
+
// yellow = index 2, bit 1 set
|
|
1120
|
+
test.ok(result.add.includes('\\Flagged'));
|
|
1121
|
+
test.ok(result.add.includes('$MailFlagBit1'));
|
|
1122
|
+
test.ok(result.remove.includes('$MailFlagBit0'));
|
|
1123
|
+
test.ok(result.remove.includes('$MailFlagBit2'));
|
|
1124
|
+
test.done();
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
module.exports['Tools: getColorFlags returns null for invalid color'] = test => {
|
|
1128
|
+
test.equal(tools.getColorFlags('invalid'), null);
|
|
1129
|
+
test.equal(tools.getColorFlags('pink'), null);
|
|
1130
|
+
test.done();
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
module.exports['Tools: getColorFlags with null returns result not null'] = test => {
|
|
1134
|
+
// null input: colorCode becomes null, which is not < 0, so it falls through
|
|
1135
|
+
let result = tools.getColorFlags(null);
|
|
1136
|
+
test.ok(result);
|
|
1137
|
+
test.ok(result.remove.includes('\\Flagged'));
|
|
1138
|
+
test.done();
|
|
1139
|
+
};
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Tests for unhandled rejection prevention.
|
|
5
5
|
*
|
|
6
|
-
* When close() runs, it rejects pending promises
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* rejections escape while the caller
|
|
6
|
+
* When close() runs, it rejects pending promises synchronously. exec() and
|
|
7
|
+
* getMailboxLock() attach .catch(noop) before returning, so the rejection is
|
|
8
|
+
* observed immediately and does not trigger Node.js unhandledRejection.
|
|
9
|
+
* These tests verify that no unhandled rejections escape while the caller
|
|
10
|
+
* still receives the expected error.
|
|
10
11
|
*
|
|
11
12
|
* Key fix: exec() and getMailboxLock() are non-async, returning the promise
|
|
12
13
|
* directly (with .catch(noop)), so the caller gets the same promise object
|
|
@@ -18,8 +19,13 @@
|
|
|
18
19
|
const net = require('net');
|
|
19
20
|
const { ImapFlow } = require('../lib/imap-flow');
|
|
20
21
|
|
|
21
|
-
// Create a
|
|
22
|
-
|
|
22
|
+
// Create a mock IMAP server with optional custom behavior.
|
|
23
|
+
// options.extraCapabilities - additional capabilities (e.g., 'IDLE')
|
|
24
|
+
// options.onCommand(socket, tag, command) - custom handler; return true if handled
|
|
25
|
+
function createMockServer(options) {
|
|
26
|
+
const extraCaps = options && options.extraCapabilities ? ' ' + options.extraCapabilities : '';
|
|
27
|
+
const onCommand = options && options.onCommand;
|
|
28
|
+
|
|
23
29
|
const server = net.createServer(socket => {
|
|
24
30
|
socket.write('* OK Mock IMAP Server ready\r\n');
|
|
25
31
|
|
|
@@ -34,8 +40,12 @@ function createMockServer() {
|
|
|
34
40
|
const tag = parts[0];
|
|
35
41
|
const command = parts[1] ? parts[1].toUpperCase() : '';
|
|
36
42
|
|
|
43
|
+
if (onCommand && onCommand(socket, tag, command, line)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
if (command === 'CAPABILITY') {
|
|
38
|
-
socket.write(
|
|
48
|
+
socket.write(`* CAPABILITY IMAP4rev1 AUTH=PLAIN${extraCaps}\r\n`);
|
|
39
49
|
socket.write(`${tag} OK CAPABILITY completed\r\n`);
|
|
40
50
|
} else if (command === 'LOGIN') {
|
|
41
51
|
socket.write(`${tag} OK LOGIN completed\r\n`);
|
|
@@ -53,6 +63,12 @@ function createMockServer() {
|
|
|
53
63
|
} else if (command === 'ID') {
|
|
54
64
|
socket.write('* ID NIL\r\n');
|
|
55
65
|
socket.write(`${tag} OK ID completed\r\n`);
|
|
66
|
+
} else if (command === 'SELECT' || command === 'EXAMINE') {
|
|
67
|
+
socket.write('* 1 EXISTS\r\n');
|
|
68
|
+
socket.write('* 1 RECENT\r\n');
|
|
69
|
+
socket.write('* OK [UIDVALIDITY 1] UIDs valid\r\n');
|
|
70
|
+
socket.write('* OK [UIDNEXT 2] Predicted next UID\r\n');
|
|
71
|
+
socket.write(`${tag} OK SELECT completed\r\n`);
|
|
56
72
|
} else if (command === 'NOOP') {
|
|
57
73
|
socket.write(`${tag} OK NOOP completed\r\n`);
|
|
58
74
|
} else if (tag && command) {
|
|
@@ -103,9 +119,8 @@ exports['Unhandled Rejection Prevention'] = {
|
|
|
103
119
|
const detector = installRejectionDetector(test);
|
|
104
120
|
|
|
105
121
|
// Create a pending request, then immediately close.
|
|
106
|
-
// close() rejects the request
|
|
107
|
-
//
|
|
108
|
-
// before the caller's handler is attached.
|
|
122
|
+
// close() rejects the request synchronously. The .catch(noop) on the
|
|
123
|
+
// exec() promise ensures the rejection is observed immediately.
|
|
109
124
|
let promise = client.exec('NOOP');
|
|
110
125
|
client.close();
|
|
111
126
|
|
|
@@ -246,7 +261,7 @@ exports['Unhandled Rejection Prevention'] = {
|
|
|
246
261
|
await client.connect();
|
|
247
262
|
|
|
248
263
|
// Request a lock, then immediately close before it resolves.
|
|
249
|
-
// close() rejects pending locks
|
|
264
|
+
// close() rejects pending locks synchronously.
|
|
250
265
|
let lockPromise = client.getMailboxLock('INBOX');
|
|
251
266
|
client.close();
|
|
252
267
|
|
|
@@ -323,5 +338,129 @@ exports['Unhandled Rejection Prevention'] = {
|
|
|
323
338
|
}
|
|
324
339
|
);
|
|
325
340
|
});
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
'BYE during IDLE should not cause unhandled rejection (Death 1)'(test) {
|
|
344
|
+
test.expect(2);
|
|
345
|
+
|
|
346
|
+
const server = createMockServer({
|
|
347
|
+
extraCapabilities: 'IDLE',
|
|
348
|
+
onCommand(socket, tag, command) {
|
|
349
|
+
if (command === 'IDLE') {
|
|
350
|
+
socket.write('+ idling\r\n');
|
|
351
|
+
// After a short delay, send BYE and close (simulates token expiry)
|
|
352
|
+
setTimeout(() => {
|
|
353
|
+
try {
|
|
354
|
+
socket.write('* BYE Session invalidated - AccessTokenExpired\r\n');
|
|
355
|
+
socket.end();
|
|
356
|
+
} catch {
|
|
357
|
+
// socket may already be closed
|
|
358
|
+
}
|
|
359
|
+
}, 50);
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
server.listen(0, '127.0.0.1', async () => {
|
|
366
|
+
const port = server.address().port;
|
|
367
|
+
|
|
368
|
+
const client = new ImapFlow({
|
|
369
|
+
host: '127.0.0.1',
|
|
370
|
+
port,
|
|
371
|
+
secure: false,
|
|
372
|
+
logger: false,
|
|
373
|
+
auth: {
|
|
374
|
+
user: 'test',
|
|
375
|
+
pass: 'test'
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const detector = installRejectionDetector(test);
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
await client.connect();
|
|
383
|
+
await client.mailboxOpen('INBOX');
|
|
384
|
+
|
|
385
|
+
// Start IDLE and wait for the BYE-triggered close
|
|
386
|
+
await new Promise((resolve, reject) => {
|
|
387
|
+
client.idle().catch(() => {
|
|
388
|
+
// Expected: IDLE rejects when BYE arrives
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
client.on('close', () => {
|
|
392
|
+
// Wait for any deferred unhandled rejections to surface
|
|
393
|
+
setTimeout(resolve, 100);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Safety timeout
|
|
397
|
+
setTimeout(() => reject(new Error('Timeout waiting for close')), 5000);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
detector.check();
|
|
401
|
+
test.ok(true, 'No unhandled rejection during IDLE + BYE');
|
|
402
|
+
} catch (err) {
|
|
403
|
+
detector.check();
|
|
404
|
+
test.ok(false, 'Unexpected error: ' + err.message);
|
|
405
|
+
} finally {
|
|
406
|
+
server.close(() => test.done());
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
'BAD response to FETCH should not cause unhandled rejection (Death 2)'(test) {
|
|
412
|
+
test.expect(3);
|
|
413
|
+
|
|
414
|
+
const server = createMockServer({
|
|
415
|
+
onCommand(socket, tag, command) {
|
|
416
|
+
if (command === 'UID' || command === 'FETCH') {
|
|
417
|
+
// Simulate "Server Unavailable" error for any FETCH variant
|
|
418
|
+
socket.write(`${tag} BAD Server Unavailable. 15\r\n`);
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
server.listen(0, '127.0.0.1', async () => {
|
|
425
|
+
const port = server.address().port;
|
|
426
|
+
|
|
427
|
+
const client = new ImapFlow({
|
|
428
|
+
host: '127.0.0.1',
|
|
429
|
+
port,
|
|
430
|
+
secure: false,
|
|
431
|
+
logger: false,
|
|
432
|
+
auth: {
|
|
433
|
+
user: 'test',
|
|
434
|
+
pass: 'test'
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const detector = installRejectionDetector(test);
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
await client.connect();
|
|
442
|
+
await client.mailboxOpen('INBOX');
|
|
443
|
+
|
|
444
|
+
// Attempt a FETCH that will get BAD response
|
|
445
|
+
try {
|
|
446
|
+
await client.fetchOne('*', { uid: true }, { uid: true });
|
|
447
|
+
test.ok(false, 'fetchOne should have rejected');
|
|
448
|
+
} catch (err) {
|
|
449
|
+
test.equal(err.message, 'Command failed', 'Should get Command failed error');
|
|
450
|
+
test.equal(err.responseStatus, 'BAD', 'Response status should be BAD');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Wait for any deferred unhandled rejections
|
|
454
|
+
await new Promise(r => setTimeout(r, 100));
|
|
455
|
+
detector.check();
|
|
456
|
+
|
|
457
|
+
client.close();
|
|
458
|
+
} catch (err) {
|
|
459
|
+
detector.check();
|
|
460
|
+
test.ok(false, 'Unexpected error: ' + err.message);
|
|
461
|
+
} finally {
|
|
462
|
+
server.close(() => test.done());
|
|
463
|
+
}
|
|
464
|
+
});
|
|
326
465
|
}
|
|
327
466
|
};
|