imapflow 1.2.13 → 1.2.14

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
+ };
@@ -1569,3 +1569,97 @@ module.exports['Connection Edge: compress _deflate error after close does not cr
1569
1569
 
1570
1570
  test.done();
1571
1571
  };
1572
+
1573
+ // ============================================
1574
+ // Mailbox Lock Tests
1575
+ // ============================================
1576
+
1577
+ module.exports['Connection Edge: getMailboxLock queues lock in locks array'] = async test => {
1578
+ let client = new ImapFlow({
1579
+ host: 'imap.example.com',
1580
+ port: 993,
1581
+ auth: { user: 'test', pass: 'test' }
1582
+ });
1583
+
1584
+ // getMailboxLock should push to locks array then reject (no connection)
1585
+ try {
1586
+ await client.getMailboxLock('INBOX');
1587
+ test.ok(false, 'Should have thrown');
1588
+ } catch (err) {
1589
+ test.ok(err);
1590
+ test.equal(err.code, 'NoConnection');
1591
+ }
1592
+
1593
+ test.done();
1594
+ };
1595
+
1596
+ module.exports['Connection Edge: lockCounter increments with each getMailboxLock call'] = async test => {
1597
+ let client = new ImapFlow({
1598
+ host: 'imap.example.com',
1599
+ port: 993,
1600
+ auth: { user: 'test', pass: 'test' }
1601
+ });
1602
+
1603
+ let initial = client.lockCounter;
1604
+ let p1 = client.getMailboxLock('INBOX').catch(() => {});
1605
+ let p2 = client.getMailboxLock('Sent').catch(() => {});
1606
+
1607
+ test.equal(client.lockCounter, initial + 2);
1608
+
1609
+ await Promise.all([p1, p2]);
1610
+ test.done();
1611
+ };
1612
+
1613
+ module.exports['Connection Edge: lock rejected when connection not usable'] = async test => {
1614
+ let client = new ImapFlow({
1615
+ host: 'imap.example.com',
1616
+ port: 993,
1617
+ auth: { user: 'test', pass: 'test' }
1618
+ });
1619
+
1620
+ // usable is false by default, no socket
1621
+ try {
1622
+ await client.getMailboxLock('INBOX');
1623
+ test.ok(false, 'Should have thrown');
1624
+ } catch (err) {
1625
+ test.ok(err);
1626
+ test.equal(err.code, 'NoConnection');
1627
+ test.ok(err.message.includes('not available'));
1628
+ }
1629
+ test.done();
1630
+ };
1631
+
1632
+ module.exports['Connection Edge: processLocks with empty locks array completes'] = async test => {
1633
+ let client = new ImapFlow({
1634
+ host: 'imap.example.com',
1635
+ port: 993,
1636
+ auth: { user: 'test', pass: 'test' }
1637
+ });
1638
+
1639
+ // processLocks with no pending locks should complete without error
1640
+ await client.processLocks();
1641
+ test.equal(client.processingLock, false);
1642
+ test.done();
1643
+ };
1644
+
1645
+ module.exports['Connection Edge: multiple locks rejected when no connection'] = async test => {
1646
+ let client = new ImapFlow({
1647
+ host: 'imap.example.com',
1648
+ port: 993,
1649
+ auth: { user: 'test', pass: 'test' }
1650
+ });
1651
+
1652
+ // Queue multiple locks, all should reject
1653
+ let errors = [];
1654
+ let p1 = client.getMailboxLock('INBOX').catch(err => errors.push(err));
1655
+ let p2 = client.getMailboxLock('Sent').catch(err => errors.push(err));
1656
+ let p3 = client.getMailboxLock('Drafts').catch(err => errors.push(err));
1657
+
1658
+ await Promise.all([p1, p2, p3]);
1659
+
1660
+ test.equal(errors.length, 3);
1661
+ for (let err of errors) {
1662
+ test.equal(err.code, 'NoConnection');
1663
+ }
1664
+ test.done();
1665
+ };
@@ -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
+ });