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.
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ const { resolveCharset } = require('../lib/charsets');
4
+
5
+ // ============================================
6
+ // Canonical name resolution
7
+ // ============================================
8
+
9
+ module.exports['Charsets: resolves canonical UTF-8'] = test => {
10
+ test.equal(resolveCharset('UTF-8'), 'UTF-8');
11
+ test.done();
12
+ };
13
+
14
+ module.exports['Charsets: resolves canonical ISO-8859-1'] = test => {
15
+ test.equal(resolveCharset('ISO-8859-1'), 'ISO-8859-1');
16
+ test.done();
17
+ };
18
+
19
+ module.exports['Charsets: resolves canonical windows-1252'] = test => {
20
+ test.equal(resolveCharset('windows-1252'), 'windows-1252');
21
+ test.done();
22
+ };
23
+
24
+ module.exports['Charsets: resolves canonical US-ASCII'] = test => {
25
+ test.equal(resolveCharset('US-ASCII'), 'US-ASCII');
26
+ test.done();
27
+ };
28
+
29
+ // ============================================
30
+ // Alias resolution
31
+ // ============================================
32
+
33
+ module.exports['Charsets: resolves utf8 alias to UTF-8'] = test => {
34
+ test.equal(resolveCharset('utf8'), 'UTF-8');
35
+ test.done();
36
+ };
37
+
38
+ module.exports['Charsets: resolves win1252 alias to windows-1252'] = test => {
39
+ test.equal(resolveCharset('win1252'), 'windows-1252');
40
+ test.done();
41
+ };
42
+
43
+ module.exports['Charsets: resolves latin1 alias to ISO-8859-1'] = test => {
44
+ test.equal(resolveCharset('latin1'), 'ISO-8859-1');
45
+ test.done();
46
+ };
47
+
48
+ module.exports['Charsets: resolves ascii alias to US-ASCII'] = test => {
49
+ test.equal(resolveCharset('ascii'), 'US-ASCII');
50
+ test.done();
51
+ };
52
+
53
+ module.exports['Charsets: resolves usascii alias to US-ASCII'] = test => {
54
+ test.equal(resolveCharset('usascii'), 'US-ASCII');
55
+ test.done();
56
+ };
57
+
58
+ // ============================================
59
+ // Case insensitivity
60
+ // ============================================
61
+
62
+ module.exports['Charsets: resolves lowercase utf-8 to UTF-8'] = test => {
63
+ test.equal(resolveCharset('utf-8'), 'UTF-8');
64
+ test.done();
65
+ };
66
+
67
+ module.exports['Charsets: resolves mixed-case Utf-8 to UTF-8'] = test => {
68
+ test.equal(resolveCharset('Utf-8'), 'UTF-8');
69
+ test.done();
70
+ };
71
+
72
+ module.exports['Charsets: resolves uppercase UTF8 (no separator) to UTF-8'] = test => {
73
+ test.equal(resolveCharset('UTF8'), 'UTF-8');
74
+ test.done();
75
+ };
76
+
77
+ // ============================================
78
+ // Separator stripping
79
+ // ============================================
80
+
81
+ module.exports['Charsets: resolves ISO_8859_1 (underscores) to ISO-8859-1'] = test => {
82
+ test.equal(resolveCharset('ISO_8859_1'), 'ISO-8859-1');
83
+ test.done();
84
+ };
85
+
86
+ module.exports['Charsets: resolves windows_1252 (underscores) to windows-1252'] = test => {
87
+ test.equal(resolveCharset('windows_1252'), 'windows-1252');
88
+ test.done();
89
+ };
90
+
91
+ // ============================================
92
+ // Japanese and CJK charsets
93
+ // ============================================
94
+
95
+ module.exports['Charsets: resolves ISO-2022-JP'] = test => {
96
+ test.equal(resolveCharset('ISO-2022-JP'), 'ISO-2022-JP');
97
+ test.done();
98
+ };
99
+
100
+ module.exports['Charsets: resolves Shift_JIS'] = test => {
101
+ test.equal(resolveCharset('Shift_JIS'), 'Shift_JIS');
102
+ test.done();
103
+ };
104
+
105
+ module.exports['Charsets: resolves EUC-JP'] = test => {
106
+ test.equal(resolveCharset('EUC-JP'), 'EUC-JP');
107
+ test.done();
108
+ };
109
+
110
+ module.exports['Charsets: resolves EUC-KR'] = test => {
111
+ test.equal(resolveCharset('EUC-KR'), 'EUC-KR');
112
+ test.done();
113
+ };
114
+
115
+ module.exports['Charsets: resolves GB2312'] = test => {
116
+ test.equal(resolveCharset('GB2312'), 'GB2312');
117
+ test.done();
118
+ };
119
+
120
+ module.exports['Charsets: resolves Big5'] = test => {
121
+ test.equal(resolveCharset('Big5'), 'Big5');
122
+ test.done();
123
+ };
124
+
125
+ module.exports['Charsets: resolves GBK'] = test => {
126
+ test.equal(resolveCharset('GBK'), 'GBK');
127
+ test.done();
128
+ };
129
+
130
+ // ============================================
131
+ // Unknown charsets return null
132
+ // ============================================
133
+
134
+ module.exports['Charsets: returns null for unknown charset x-unknown'] = test => {
135
+ test.strictEqual(resolveCharset('x-unknown'), null);
136
+ test.done();
137
+ };
138
+
139
+ module.exports['Charsets: returns null for unknown charset bogus'] = test => {
140
+ test.strictEqual(resolveCharset('bogus'), null);
141
+ test.done();
142
+ };
143
+
144
+ module.exports['Charsets: returns null for empty string'] = test => {
145
+ test.strictEqual(resolveCharset(''), null);
146
+ test.done();
147
+ };
148
+
149
+ // ============================================
150
+ // Invalid input throws TypeError
151
+ // ============================================
152
+
153
+ module.exports['Charsets: throws TypeError for null input'] = test => {
154
+ test.throws(() => resolveCharset(null));
155
+ test.done();
156
+ };
157
+
158
+ module.exports['Charsets: throws TypeError for undefined input'] = test => {
159
+ test.throws(() => resolveCharset(undefined));
160
+ test.done();
161
+ };
@@ -8247,3 +8247,64 @@ module.exports['Commands: enable updates connection.enabled'] = async test => {
8247
8247
  test.ok(!connection.enabled.has('EXISTING')); // Old value should be gone
8248
8248
  test.done();
8249
8249
  };
8250
+
8251
+ // ============================================
8252
+ // Download / DownloadMany Tests
8253
+ // ============================================
8254
+
8255
+ const { ImapFlow } = require('../lib/imap-flow');
8256
+
8257
+ module.exports['Commands: download returns empty when no mailbox selected'] = async test => {
8258
+ let client = new ImapFlow({
8259
+ host: 'imap.example.com',
8260
+ port: 993,
8261
+ auth: { user: 'test', pass: 'test' }
8262
+ });
8263
+ // mailbox is not set by default
8264
+ let result = await client.download('1', '1.2');
8265
+ test.deepEqual(result, {});
8266
+ test.done();
8267
+ };
8268
+
8269
+ module.exports['Commands: downloadMany returns empty when no mailbox selected'] = async test => {
8270
+ let client = new ImapFlow({
8271
+ host: 'imap.example.com',
8272
+ port: 993,
8273
+ auth: { user: 'test', pass: 'test' }
8274
+ });
8275
+ let result = await client.downloadMany('1', ['2', '3']);
8276
+ test.deepEqual(result, {});
8277
+ test.done();
8278
+ };
8279
+
8280
+ module.exports['Commands: download with fetchOne returning null'] = async test => {
8281
+ let client = new ImapFlow({
8282
+ host: 'imap.example.com',
8283
+ port: 993,
8284
+ auth: { user: 'test', pass: 'test' }
8285
+ });
8286
+ // Set mailbox so download doesn't return {} early
8287
+ client.mailbox = { path: 'INBOX' };
8288
+ // Mock fetchOne to return null (message not found)
8289
+ client.fetchOne = async () => null;
8290
+
8291
+ // part '1' triggers the bodyStructure check path
8292
+ let result = await client.download('1', '1');
8293
+ test.equal(result.response, false);
8294
+ test.equal(result.chunk, false);
8295
+ test.done();
8296
+ };
8297
+
8298
+ module.exports['Commands: downloadMany with fetchOne returning null'] = async test => {
8299
+ let client = new ImapFlow({
8300
+ host: 'imap.example.com',
8301
+ port: 993,
8302
+ auth: { user: 'test', pass: 'test' }
8303
+ });
8304
+ client.mailbox = { path: 'INBOX' };
8305
+ client.fetchOne = async () => null;
8306
+
8307
+ let result = await client.downloadMany('1', ['2', '3']);
8308
+ test.equal(result.response, false);
8309
+ test.done();
8310
+ };
@@ -863,7 +863,7 @@ module.exports['Connection Edge: Close with empty locks array'] = test => {
863
863
  test.done();
864
864
  };
865
865
 
866
- module.exports['Connection Edge: Lock rejection is deferred via setImmediate'] = test => {
866
+ module.exports['Connection Edge: Lock rejection happens synchronously during close'] = test => {
867
867
  let client = new ImapFlow({
868
868
  host: 'imap.example.com',
869
869
  port: 993,
@@ -871,7 +871,6 @@ module.exports['Connection Edge: Lock rejection is deferred via setImmediate'] =
871
871
  });
872
872
 
873
873
  let rejectionTime = null;
874
- let closeTime = null;
875
874
 
876
875
  client.locks = [
877
876
  {
@@ -888,16 +887,10 @@ module.exports['Connection Edge: Lock rejection is deferred via setImmediate'] =
888
887
  client.usable = true;
889
888
 
890
889
  client.close();
891
- closeTime = Date.now();
892
890
 
893
- // Rejection should not have happened yet (synchronously)
894
- test.equal(rejectionTime, null, 'Rejection should be deferred');
895
-
896
- setImmediate(() => {
897
- test.ok(rejectionTime !== null, 'Rejection should happen after setImmediate');
898
- test.ok(rejectionTime >= closeTime, 'Rejection should happen after close()');
899
- test.done();
900
- });
891
+ // Rejection should have happened synchronously during close()
892
+ test.ok(rejectionTime !== null, 'Rejection should happen synchronously during close()');
893
+ test.done();
901
894
  };
902
895
 
903
896
  module.exports['Connection Edge: Pending requests and locks both rejected on close'] = test => {
@@ -940,11 +933,10 @@ module.exports['Connection Edge: Pending requests and locks both rejected on clo
940
933
 
941
934
  client.close();
942
935
 
943
- setImmediate(() => {
944
- test.ok(requestRejected, 'Pending request should be rejected');
945
- test.ok(lockRejected, 'Pending lock should be rejected');
946
- test.done();
947
- });
936
+ // Rejections happen synchronously during close()
937
+ test.ok(requestRejected, 'Pending request should be rejected');
938
+ test.ok(lockRejected, 'Pending lock should be rejected');
939
+ test.done();
948
940
  };
949
941
 
950
942
  module.exports['Connection Edge: exec throws NoConnection when in LOGOUT state'] = async test => {
@@ -1569,3 +1561,97 @@ module.exports['Connection Edge: compress _deflate error after close does not cr
1569
1561
 
1570
1562
  test.done();
1571
1563
  };
1564
+
1565
+ // ============================================
1566
+ // Mailbox Lock Tests
1567
+ // ============================================
1568
+
1569
+ module.exports['Connection Edge: getMailboxLock queues lock in locks array'] = async test => {
1570
+ let client = new ImapFlow({
1571
+ host: 'imap.example.com',
1572
+ port: 993,
1573
+ auth: { user: 'test', pass: 'test' }
1574
+ });
1575
+
1576
+ // getMailboxLock should push to locks array then reject (no connection)
1577
+ try {
1578
+ await client.getMailboxLock('INBOX');
1579
+ test.ok(false, 'Should have thrown');
1580
+ } catch (err) {
1581
+ test.ok(err);
1582
+ test.equal(err.code, 'NoConnection');
1583
+ }
1584
+
1585
+ test.done();
1586
+ };
1587
+
1588
+ module.exports['Connection Edge: lockCounter increments with each getMailboxLock call'] = async test => {
1589
+ let client = new ImapFlow({
1590
+ host: 'imap.example.com',
1591
+ port: 993,
1592
+ auth: { user: 'test', pass: 'test' }
1593
+ });
1594
+
1595
+ let initial = client.lockCounter;
1596
+ let p1 = client.getMailboxLock('INBOX').catch(() => {});
1597
+ let p2 = client.getMailboxLock('Sent').catch(() => {});
1598
+
1599
+ test.equal(client.lockCounter, initial + 2);
1600
+
1601
+ await Promise.all([p1, p2]);
1602
+ test.done();
1603
+ };
1604
+
1605
+ module.exports['Connection Edge: lock rejected when connection not usable'] = async test => {
1606
+ let client = new ImapFlow({
1607
+ host: 'imap.example.com',
1608
+ port: 993,
1609
+ auth: { user: 'test', pass: 'test' }
1610
+ });
1611
+
1612
+ // usable is false by default, no socket
1613
+ try {
1614
+ await client.getMailboxLock('INBOX');
1615
+ test.ok(false, 'Should have thrown');
1616
+ } catch (err) {
1617
+ test.ok(err);
1618
+ test.equal(err.code, 'NoConnection');
1619
+ test.ok(err.message.includes('not available'));
1620
+ }
1621
+ test.done();
1622
+ };
1623
+
1624
+ module.exports['Connection Edge: processLocks with empty locks array completes'] = async test => {
1625
+ let client = new ImapFlow({
1626
+ host: 'imap.example.com',
1627
+ port: 993,
1628
+ auth: { user: 'test', pass: 'test' }
1629
+ });
1630
+
1631
+ // processLocks with no pending locks should complete without error
1632
+ await client.processLocks();
1633
+ test.equal(client.processingLock, false);
1634
+ test.done();
1635
+ };
1636
+
1637
+ module.exports['Connection Edge: multiple locks rejected when no connection'] = async test => {
1638
+ let client = new ImapFlow({
1639
+ host: 'imap.example.com',
1640
+ port: 993,
1641
+ auth: { user: 'test', pass: 'test' }
1642
+ });
1643
+
1644
+ // Queue multiple locks, all should reject
1645
+ let errors = [];
1646
+ let p1 = client.getMailboxLock('INBOX').catch(err => errors.push(err));
1647
+ let p2 = client.getMailboxLock('Sent').catch(err => errors.push(err));
1648
+ let p3 = client.getMailboxLock('Drafts').catch(err => errors.push(err));
1649
+
1650
+ await Promise.all([p1, p2, p3]);
1651
+
1652
+ test.equal(errors.length, 3);
1653
+ for (let err of errors) {
1654
+ test.equal(err.code, 'NoConnection');
1655
+ }
1656
+ test.done();
1657
+ };
@@ -0,0 +1,153 @@
1
+ 'use strict';
2
+
3
+ const { parseCopyUid } = require('../lib/commands/copyuid-parser');
4
+
5
+ /**
6
+ * Builds a minimal IMAP response object with a COPYUID-style section.
7
+ *
8
+ * @param {Array} section - Array of section elements with value properties
9
+ * @returns {Object} Response object suitable for parseCopyUid
10
+ */
11
+ function makeResponse(section) {
12
+ return { attributes: [{ section }] };
13
+ }
14
+
15
+ // ============================================
16
+ // Single UID pair
17
+ // ============================================
18
+
19
+ module.exports['CopyUID Parser: single UID pair sets uidValidity and uidMap entry'] = test => {
20
+ let map = {};
21
+ parseCopyUid(makeResponse([{ value: 'COPYUID' }, { value: '12345' }, { value: '1' }, { value: '100' }]), map);
22
+
23
+ test.equal(map.uidValidity, BigInt('12345'));
24
+ test.ok(map.uidMap instanceof Map);
25
+ test.equal(map.uidMap.get(1), 100);
26
+ test.equal(map.uidMap.size, 1);
27
+ test.done();
28
+ };
29
+
30
+ // ============================================
31
+ // UID ranges
32
+ // ============================================
33
+
34
+ module.exports['CopyUID Parser: contiguous UID range maps all source UIDs to destinations'] = test => {
35
+ let map = {};
36
+ parseCopyUid(makeResponse([{ value: 'COPYUID' }, { value: '99' }, { value: '1:3' }, { value: '10:12' }]), map);
37
+
38
+ test.equal(map.uidValidity, BigInt('99'));
39
+ test.ok(map.uidMap instanceof Map);
40
+ test.equal(map.uidMap.size, 3);
41
+ test.equal(map.uidMap.get(1), 10);
42
+ test.equal(map.uidMap.get(2), 11);
43
+ test.equal(map.uidMap.get(3), 12);
44
+ test.done();
45
+ };
46
+
47
+ // ============================================
48
+ // Comma-separated UIDs
49
+ // ============================================
50
+
51
+ module.exports['CopyUID Parser: comma-separated UIDs map each source to its destination'] = test => {
52
+ let map = {};
53
+ parseCopyUid(makeResponse([{ value: 'COPYUID' }, { value: '200' }, { value: '1,5,9' }, { value: '100,105,109' }]), map);
54
+
55
+ test.equal(map.uidMap.size, 3);
56
+ test.equal(map.uidMap.get(1), 100);
57
+ test.equal(map.uidMap.get(5), 105);
58
+ test.equal(map.uidMap.get(9), 109);
59
+ test.done();
60
+ };
61
+
62
+ // ============================================
63
+ // Non-COPYUID response code
64
+ // ============================================
65
+
66
+ module.exports['CopyUID Parser: non-COPYUID response code leaves map unchanged'] = test => {
67
+ let map = {};
68
+ parseCopyUid(makeResponse([{ value: 'APPENDUID' }, { value: '12345' }, { value: '1' }, { value: '100' }]), map);
69
+
70
+ test.equal(map.uidValidity, undefined);
71
+ test.equal(map.uidMap, undefined);
72
+ test.done();
73
+ };
74
+
75
+ // ============================================
76
+ // Missing / malformed input
77
+ // ============================================
78
+
79
+ module.exports['CopyUID Parser: missing attributes leaves map unchanged'] = test => {
80
+ let map = {};
81
+ parseCopyUid({}, map);
82
+
83
+ test.equal(map.uidValidity, undefined);
84
+ test.equal(map.uidMap, undefined);
85
+ test.done();
86
+ };
87
+
88
+ module.exports['CopyUID Parser: empty attributes array leaves map unchanged'] = test => {
89
+ let map = {};
90
+ parseCopyUid({ attributes: [] }, map);
91
+
92
+ test.equal(map.uidValidity, undefined);
93
+ test.equal(map.uidMap, undefined);
94
+ test.done();
95
+ };
96
+
97
+ module.exports['CopyUID Parser: null section leaves map unchanged'] = test => {
98
+ let map = {};
99
+ parseCopyUid({ attributes: [{ section: null }] }, map);
100
+
101
+ test.equal(map.uidValidity, undefined);
102
+ test.equal(map.uidMap, undefined);
103
+ test.done();
104
+ };
105
+
106
+ // ============================================
107
+ // Non-numeric uidValidity
108
+ // ============================================
109
+
110
+ module.exports['CopyUID Parser: non-numeric uidValidity skips uidValidity but still builds uidMap'] = test => {
111
+ let map = {};
112
+ parseCopyUid(makeResponse([{ value: 'COPYUID' }, { value: 'abc' }, { value: '1' }, { value: '100' }]), map);
113
+
114
+ test.equal(map.uidValidity, undefined);
115
+ test.ok(map.uidMap instanceof Map);
116
+ test.equal(map.uidMap.get(1), 100);
117
+ test.done();
118
+ };
119
+
120
+ // ============================================
121
+ // Mismatched source/destination lengths
122
+ // ============================================
123
+
124
+ module.exports['CopyUID Parser: mismatched source and destination lengths sets uidValidity but skips uidMap'] = test => {
125
+ let map = {};
126
+ parseCopyUid(makeResponse([{ value: 'COPYUID' }, { value: '12345' }, { value: '1:3' }, { value: '10' }]), map);
127
+
128
+ test.equal(map.uidValidity, BigInt('12345'));
129
+ test.equal(map.uidMap, undefined);
130
+ test.done();
131
+ };
132
+
133
+ // ============================================
134
+ // Large uidValidity
135
+ // ============================================
136
+
137
+ module.exports['CopyUID Parser: large uidValidity is stored as BigInt'] = test => {
138
+ let map = {};
139
+ parseCopyUid(makeResponse([{ value: 'COPYUID' }, { value: '999999999999' }, { value: '1' }, { value: '100' }]), map);
140
+
141
+ test.equal(map.uidValidity, BigInt('999999999999'));
142
+ test.done();
143
+ };
144
+
145
+ module.exports['CopyUID Parser: uidValidity zero is stored as BigInt(0)'] = test => {
146
+ let map = {};
147
+ parseCopyUid(makeResponse([{ value: 'COPYUID' }, { value: '0' }, { value: '1' }, { value: '100' }]), map);
148
+
149
+ test.strictEqual(map.uidValidity, BigInt(0));
150
+ test.ok(map.uidMap instanceof Map);
151
+ test.equal(map.uidMap.get(1), 100);
152
+ test.done();
153
+ };
@@ -441,3 +441,69 @@ module.exports['IMAP Compiler: LITERAL byte length'] = test =>
441
441
  '* CMD "(* 10B literal *)" "Vana kere"'
442
442
  )
443
443
  );
444
+
445
+ module.exports['IMAP Compiler: MongoDB binary object is unwrapped'] = test =>
446
+ asyncWrapper(test, async test => {
447
+ // Create a non-Buffer object with a .buffer property (simulates MongoDB Binary)
448
+ const mongoBinary = { buffer: Buffer.from('test data') };
449
+ const compiled = (
450
+ await compiler({
451
+ tag: '*',
452
+ command: 'CMD',
453
+ attributes: [mongoBinary]
454
+ })
455
+ ).toString();
456
+ // The buffer content should appear in the output (unwrapped from the MongoDB binary wrapper)
457
+ test.ok(compiled.includes('test data'), 'should contain the buffer content after unwrapping');
458
+ test.ok(compiled.includes('CMD'), 'should contain command');
459
+ });
460
+
461
+ module.exports['IMAP Compiler: long string truncated in logging mode'] = test =>
462
+ asyncWrapper(test, async test => {
463
+ const longString = 'x'.repeat(150);
464
+ const compiled = (
465
+ await compiler(
466
+ {
467
+ tag: '*',
468
+ command: 'CMD',
469
+ attributes: [longString]
470
+ },
471
+ { asArray: false, isLogging: true }
472
+ )
473
+ ).toString();
474
+ test.ok(compiled.includes('150B string'), 'should show byte count instead of content');
475
+ test.ok(!compiled.includes('x'.repeat(150)), 'should not contain the full string');
476
+ });
477
+
478
+ module.exports['IMAP Compiler: literal truncated in logging mode'] = test =>
479
+ asyncWrapper(test, async test => {
480
+ const compiled = (
481
+ await compiler(
482
+ {
483
+ tag: '*',
484
+ command: 'CMD',
485
+ attributes: [{ type: 'LITERAL', value: Buffer.alloc(200) }]
486
+ },
487
+ { asArray: false, isLogging: true }
488
+ )
489
+ ).toString();
490
+ test.ok(compiled.includes('200B literal'), 'should show byte count for literal');
491
+ });
492
+
493
+ module.exports['IMAP Compiler: partial range in SECTION'] = test =>
494
+ asyncWrapper(test, async test => {
495
+ const compiled = (
496
+ await compiler({
497
+ tag: '*',
498
+ command: 'CMD',
499
+ attributes: [
500
+ {
501
+ type: 'SECTION',
502
+ section: [{ type: 'ATOM', value: 'BODY' }],
503
+ partial: [0, 50]
504
+ }
505
+ ]
506
+ })
507
+ ).toString();
508
+ test.ok(compiled.includes('<0.50>'), 'should contain partial range <0.50>');
509
+ });