imapflow 1.2.4 → 1.2.6

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,992 @@
1
+ 'use strict';
2
+
3
+ const tools = require('../lib/tools');
4
+
5
+ // Mock connection for testing
6
+ let createMockConnection = (options = {}) => ({
7
+ enabled: new Set(options.utf8 ? ['UTF8=ACCEPT'] : []),
8
+ namespace: options.namespace || null
9
+ });
10
+
11
+ // ============================================
12
+ // encodePath / decodePath tests
13
+ // ============================================
14
+
15
+ module.exports['Tools: encodePath with ASCII path'] = test => {
16
+ let connection = createMockConnection();
17
+ let result = tools.encodePath(connection, 'INBOX');
18
+ test.equal(result, 'INBOX');
19
+ test.done();
20
+ };
21
+
22
+ module.exports['Tools: encodePath with Unicode path (no UTF8)'] = test => {
23
+ let connection = createMockConnection({ utf8: false });
24
+ let result = tools.encodePath(connection, 'Sent/Gesendete');
25
+ // ASCII path should remain unchanged
26
+ test.equal(result, 'Sent/Gesendete');
27
+ test.done();
28
+ };
29
+
30
+ module.exports['Tools: encodePath with Unicode when UTF8=ACCEPT enabled'] = test => {
31
+ let connection = createMockConnection({ utf8: true });
32
+ let result = tools.encodePath(connection, 'Posteingang/Ordner');
33
+ // With UTF8=ACCEPT, path should remain unchanged
34
+ test.equal(result, 'Posteingang/Ordner');
35
+ test.done();
36
+ };
37
+
38
+ module.exports['Tools: encodePath with null/undefined'] = test => {
39
+ let connection = createMockConnection();
40
+ test.equal(tools.encodePath(connection, null), '');
41
+ test.equal(tools.encodePath(connection, undefined), '');
42
+ test.done();
43
+ };
44
+
45
+ module.exports['Tools: decodePath with ASCII path'] = test => {
46
+ let connection = createMockConnection();
47
+ let result = tools.decodePath(connection, 'INBOX');
48
+ test.equal(result, 'INBOX');
49
+ test.done();
50
+ };
51
+
52
+ module.exports['Tools: decodePath with ampersand'] = test => {
53
+ let connection = createMockConnection({ utf8: false });
54
+ // UTF-7-IMAP encoded string
55
+ let result = tools.decodePath(connection, 'Test&-Folder');
56
+ test.ok(typeof result === 'string');
57
+ test.done();
58
+ };
59
+
60
+ module.exports['Tools: decodePath with UTF8=ACCEPT enabled'] = test => {
61
+ let connection = createMockConnection({ utf8: true });
62
+ let result = tools.decodePath(connection, 'Test&Folder');
63
+ // With UTF8=ACCEPT, should not decode
64
+ test.equal(result, 'Test&Folder');
65
+ test.done();
66
+ };
67
+
68
+ module.exports['Tools: decodePath with null/undefined'] = test => {
69
+ let connection = createMockConnection();
70
+ test.equal(tools.decodePath(connection, null), '');
71
+ test.equal(tools.decodePath(connection, undefined), '');
72
+ test.done();
73
+ };
74
+
75
+ // ============================================
76
+ // normalizePath tests
77
+ // ============================================
78
+
79
+ module.exports['Tools: normalizePath with INBOX (case insensitive)'] = test => {
80
+ let connection = createMockConnection();
81
+ test.equal(tools.normalizePath(connection, 'inbox'), 'INBOX');
82
+ test.equal(tools.normalizePath(connection, 'INBOX'), 'INBOX');
83
+ test.equal(tools.normalizePath(connection, 'InBox'), 'INBOX');
84
+ test.done();
85
+ };
86
+
87
+ module.exports['Tools: normalizePath with array path'] = test => {
88
+ let connection = createMockConnection({
89
+ namespace: { delimiter: '/', prefix: '' }
90
+ });
91
+ let result = tools.normalizePath(connection, ['Folder', 'Subfolder']);
92
+ test.equal(result, 'Folder/Subfolder');
93
+ test.done();
94
+ };
95
+
96
+ module.exports['Tools: normalizePath with namespace prefix'] = test => {
97
+ let connection = createMockConnection({
98
+ namespace: { delimiter: '.', prefix: 'INBOX.' }
99
+ });
100
+ let result = tools.normalizePath(connection, 'Sent');
101
+ test.equal(result, 'INBOX.Sent');
102
+ test.done();
103
+ };
104
+
105
+ module.exports['Tools: normalizePath skip namespace'] = test => {
106
+ let connection = createMockConnection({
107
+ namespace: { delimiter: '.', prefix: 'INBOX.' }
108
+ });
109
+ let result = tools.normalizePath(connection, 'Sent', true);
110
+ test.equal(result, 'Sent');
111
+ test.done();
112
+ };
113
+
114
+ module.exports['Tools: normalizePath already has prefix'] = test => {
115
+ let connection = createMockConnection({
116
+ namespace: { delimiter: '.', prefix: 'INBOX.' }
117
+ });
118
+ let result = tools.normalizePath(connection, 'INBOX.Sent');
119
+ test.equal(result, 'INBOX.Sent');
120
+ test.done();
121
+ };
122
+
123
+ // ============================================
124
+ // comparePaths tests
125
+ // ============================================
126
+
127
+ module.exports['Tools: comparePaths equal paths'] = test => {
128
+ let connection = createMockConnection();
129
+ test.equal(tools.comparePaths(connection, 'INBOX', 'INBOX'), true);
130
+ test.equal(tools.comparePaths(connection, 'inbox', 'INBOX'), true);
131
+ test.done();
132
+ };
133
+
134
+ module.exports['Tools: comparePaths different paths'] = test => {
135
+ let connection = createMockConnection();
136
+ test.equal(tools.comparePaths(connection, 'INBOX', 'Sent'), false);
137
+ test.done();
138
+ };
139
+
140
+ module.exports['Tools: comparePaths with null/undefined'] = test => {
141
+ let connection = createMockConnection();
142
+ test.equal(tools.comparePaths(connection, null, 'INBOX'), false);
143
+ test.equal(tools.comparePaths(connection, 'INBOX', null), false);
144
+ test.equal(tools.comparePaths(connection, null, null), false);
145
+ test.done();
146
+ };
147
+
148
+ // ============================================
149
+ // updateCapabilities tests
150
+ // ============================================
151
+
152
+ module.exports['Tools: updateCapabilities with valid list'] = test => {
153
+ let list = [{ value: 'IMAP4rev1' }, { value: 'IDLE' }, { value: 'NAMESPACE' }];
154
+ let result = tools.updateCapabilities(list);
155
+ test.ok(result instanceof Map);
156
+ test.equal(result.get('IMAP4rev1'), true);
157
+ test.equal(result.get('IDLE'), true);
158
+ test.equal(result.get('NAMESPACE'), true);
159
+ test.done();
160
+ };
161
+
162
+ module.exports['Tools: updateCapabilities with APPENDLIMIT'] = test => {
163
+ let list = [{ value: 'APPENDLIMIT=52428800' }];
164
+ let result = tools.updateCapabilities(list);
165
+ test.equal(result.get('APPENDLIMIT'), 52428800);
166
+ test.done();
167
+ };
168
+
169
+ module.exports['Tools: updateCapabilities with empty/null list'] = test => {
170
+ test.ok(tools.updateCapabilities(null) instanceof Map);
171
+ test.ok(tools.updateCapabilities([]) instanceof Map);
172
+ test.ok(tools.updateCapabilities(undefined) instanceof Map);
173
+ test.done();
174
+ };
175
+
176
+ module.exports['Tools: updateCapabilities skips non-string values'] = test => {
177
+ let list = [{ value: 'IDLE' }, { value: 123 }, { value: null }];
178
+ let result = tools.updateCapabilities(list);
179
+ test.equal(result.get('IDLE'), true);
180
+ test.equal(result.size, 1);
181
+ test.done();
182
+ };
183
+
184
+ // ============================================
185
+ // getStatusCode tests
186
+ // ============================================
187
+
188
+ module.exports['Tools: getStatusCode with valid response'] = test => {
189
+ let response = {
190
+ attributes: [
191
+ {
192
+ section: [{ value: 'TRYCREATE' }]
193
+ }
194
+ ]
195
+ };
196
+ test.equal(tools.getStatusCode(response), 'TRYCREATE');
197
+ test.done();
198
+ };
199
+
200
+ module.exports['Tools: getStatusCode with null/invalid response'] = test => {
201
+ test.equal(tools.getStatusCode(null), false);
202
+ test.equal(tools.getStatusCode({}), false);
203
+ test.equal(tools.getStatusCode({ attributes: [] }), false);
204
+ test.equal(tools.getStatusCode({ attributes: [{}] }), false);
205
+ test.done();
206
+ };
207
+
208
+ // ============================================
209
+ // getErrorText tests
210
+ // ============================================
211
+
212
+ module.exports['Tools: getErrorText with null response'] = async test => {
213
+ let result = await tools.getErrorText(null);
214
+ test.equal(result, false);
215
+ test.done();
216
+ };
217
+
218
+ module.exports['Tools: getErrorText with valid response'] = async test => {
219
+ let response = {
220
+ tag: '*',
221
+ command: 'OK',
222
+ attributes: [{ type: 'TEXT', value: 'Success' }]
223
+ };
224
+ let result = await tools.getErrorText(response);
225
+ test.ok(typeof result === 'string');
226
+ test.done();
227
+ };
228
+
229
+ // ============================================
230
+ // getFlagColor tests
231
+ // ============================================
232
+
233
+ module.exports['Tools: getFlagColor without Flagged'] = test => {
234
+ let flags = new Set(['\\Seen']);
235
+ test.equal(tools.getFlagColor(flags), null);
236
+ test.done();
237
+ };
238
+
239
+ module.exports['Tools: getFlagColor with Flagged only (red)'] = test => {
240
+ let flags = new Set(['\\Flagged']);
241
+ test.equal(tools.getFlagColor(flags), 'red');
242
+ test.done();
243
+ };
244
+
245
+ module.exports['Tools: getFlagColor with color bits'] = test => {
246
+ // bit0=1, bit1=0, bit2=0 => orange (index 1)
247
+ let flags = new Set(['\\Flagged', '$MailFlagBit0']);
248
+ test.equal(tools.getFlagColor(flags), 'orange');
249
+
250
+ // bit0=0, bit1=1, bit2=0 => yellow (index 2)
251
+ flags = new Set(['\\Flagged', '$MailFlagBit1']);
252
+ test.equal(tools.getFlagColor(flags), 'yellow');
253
+
254
+ // bit0=1, bit1=1, bit2=0 => green (index 3)
255
+ flags = new Set(['\\Flagged', '$MailFlagBit0', '$MailFlagBit1']);
256
+ test.equal(tools.getFlagColor(flags), 'green');
257
+
258
+ // bit0=0, bit1=0, bit2=1 => blue (index 4)
259
+ flags = new Set(['\\Flagged', '$MailFlagBit2']);
260
+ test.equal(tools.getFlagColor(flags), 'blue');
261
+
262
+ // bit0=1, bit1=0, bit2=1 => purple (index 5)
263
+ flags = new Set(['\\Flagged', '$MailFlagBit0', '$MailFlagBit2']);
264
+ test.equal(tools.getFlagColor(flags), 'purple');
265
+
266
+ // bit0=0, bit1=1, bit2=1 => grey (index 6)
267
+ flags = new Set(['\\Flagged', '$MailFlagBit1', '$MailFlagBit2']);
268
+ test.equal(tools.getFlagColor(flags), 'grey');
269
+
270
+ test.done();
271
+ };
272
+
273
+ // ============================================
274
+ // getColorFlags tests
275
+ // ============================================
276
+
277
+ module.exports['Tools: getColorFlags with valid color'] = test => {
278
+ // 'orange' is index 1, which is truthy
279
+ let result = tools.getColorFlags('orange');
280
+ test.ok(Array.isArray(result.add));
281
+ test.ok(Array.isArray(result.remove));
282
+ test.ok(result.add.includes('\\Flagged'));
283
+ test.done();
284
+ };
285
+
286
+ module.exports['Tools: getColorFlags with red (index 0)'] = test => {
287
+ // 'red' is index 0, which is falsy in JS - removes \\Flagged
288
+ let result = tools.getColorFlags('red');
289
+ test.ok(Array.isArray(result.add));
290
+ test.ok(Array.isArray(result.remove));
291
+ test.ok(result.remove.includes('\\Flagged'));
292
+ test.done();
293
+ };
294
+
295
+ module.exports['Tools: getColorFlags with null (remove flag)'] = test => {
296
+ let result = tools.getColorFlags(null);
297
+ test.ok(result.remove.includes('\\Flagged'));
298
+ test.done();
299
+ };
300
+
301
+ module.exports['Tools: getColorFlags with invalid color'] = test => {
302
+ let result = tools.getColorFlags('invalid-color');
303
+ test.equal(result, null);
304
+ test.done();
305
+ };
306
+
307
+ module.exports['Tools: getColorFlags sets correct bits'] = test => {
308
+ // orange = index 1 = bit0 set
309
+ let result = tools.getColorFlags('orange');
310
+ test.ok(result.add.includes('$MailFlagBit0'));
311
+ test.ok(result.remove.includes('$MailFlagBit1'));
312
+ test.ok(result.remove.includes('$MailFlagBit2'));
313
+
314
+ // green = index 3 = bit0 + bit1 set
315
+ result = tools.getColorFlags('green');
316
+ test.ok(result.add.includes('$MailFlagBit0'));
317
+ test.ok(result.add.includes('$MailFlagBit1'));
318
+ test.ok(result.remove.includes('$MailFlagBit2'));
319
+
320
+ test.done();
321
+ };
322
+
323
+ // ============================================
324
+ // isDate tests
325
+ // ============================================
326
+
327
+ module.exports['Tools: isDate with Date object'] = test => {
328
+ test.equal(tools.isDate(new Date()), true);
329
+ test.equal(tools.isDate(new Date('2023-01-01')), true);
330
+ test.done();
331
+ };
332
+
333
+ module.exports['Tools: isDate with non-Date'] = test => {
334
+ test.equal(tools.isDate('2023-01-01'), false);
335
+ test.equal(tools.isDate(12345), false);
336
+ test.equal(tools.isDate(null), false);
337
+ test.equal(tools.isDate({}), false);
338
+ test.done();
339
+ };
340
+
341
+ // ============================================
342
+ // formatDate tests
343
+ // ============================================
344
+
345
+ module.exports['Tools: formatDate with Date object'] = test => {
346
+ let date = new Date('2023-06-15T00:00:00.000Z');
347
+ let result = tools.formatDate(date);
348
+ test.equal(result, '15-Jun-2023');
349
+ test.done();
350
+ };
351
+
352
+ module.exports['Tools: formatDate with string'] = test => {
353
+ let result = tools.formatDate('2023-06-15');
354
+ test.equal(result, '15-Jun-2023');
355
+ test.done();
356
+ };
357
+
358
+ module.exports['Tools: formatDate with invalid date'] = test => {
359
+ let result = tools.formatDate('invalid');
360
+ test.equal(result, undefined);
361
+ test.done();
362
+ };
363
+
364
+ // ============================================
365
+ // formatDateTime tests
366
+ // ============================================
367
+
368
+ module.exports['Tools: formatDateTime with Date object'] = test => {
369
+ let date = new Date('2023-06-15T14:30:45.000Z');
370
+ let result = tools.formatDateTime(date);
371
+ test.ok(result.includes('Jun-2023'));
372
+ test.ok(result.includes('14:30:45'));
373
+ test.ok(result.includes('+0000'));
374
+ test.done();
375
+ };
376
+
377
+ module.exports['Tools: formatDateTime with null/undefined'] = test => {
378
+ test.equal(tools.formatDateTime(null), undefined);
379
+ test.equal(tools.formatDateTime(undefined), undefined);
380
+ test.done();
381
+ };
382
+
383
+ module.exports['Tools: formatDateTime with string'] = test => {
384
+ let result = tools.formatDateTime('2023-06-15T10:00:00Z');
385
+ test.ok(typeof result === 'string');
386
+ test.ok(result.includes('Jun-2023'));
387
+ test.done();
388
+ };
389
+
390
+ module.exports['Tools: formatDateTime with invalid date string'] = test => {
391
+ let result = tools.formatDateTime('invalid-date-string');
392
+ test.equal(result, undefined);
393
+ test.done();
394
+ };
395
+
396
+ // ============================================
397
+ // formatFlag tests
398
+ // ============================================
399
+
400
+ module.exports['Tools: formatFlag with standard flags'] = test => {
401
+ test.equal(tools.formatFlag('\\Seen'), '\\Seen');
402
+ test.equal(tools.formatFlag('\\SEEN'), '\\Seen');
403
+ test.equal(tools.formatFlag('\\answered'), '\\Answered');
404
+ test.equal(tools.formatFlag('\\flagged'), '\\Flagged');
405
+ test.equal(tools.formatFlag('\\deleted'), '\\Deleted');
406
+ test.equal(tools.formatFlag('\\draft'), '\\Draft');
407
+ test.done();
408
+ };
409
+
410
+ module.exports['Tools: formatFlag with Recent (cannot set)'] = test => {
411
+ test.equal(tools.formatFlag('\\Recent'), false);
412
+ test.equal(tools.formatFlag('\\recent'), false);
413
+ test.done();
414
+ };
415
+
416
+ module.exports['Tools: formatFlag with custom flags'] = test => {
417
+ test.equal(tools.formatFlag('$CustomFlag'), '$CustomFlag');
418
+ test.equal(tools.formatFlag('MyFlag'), 'MyFlag');
419
+ test.done();
420
+ };
421
+
422
+ // ============================================
423
+ // canUseFlag tests
424
+ // ============================================
425
+
426
+ module.exports['Tools: canUseFlag with no mailbox'] = test => {
427
+ test.equal(tools.canUseFlag(null, '\\Seen'), true);
428
+ test.done();
429
+ };
430
+
431
+ module.exports['Tools: canUseFlag with wildcard permanent flags'] = test => {
432
+ let mailbox = { permanentFlags: new Set(['\\*']) };
433
+ test.equal(tools.canUseFlag(mailbox, '\\Seen'), true);
434
+ test.equal(tools.canUseFlag(mailbox, '$CustomFlag'), true);
435
+ test.done();
436
+ };
437
+
438
+ module.exports['Tools: canUseFlag with specific permanent flags'] = test => {
439
+ let mailbox = { permanentFlags: new Set(['\\Seen', '\\Flagged']) };
440
+ test.equal(tools.canUseFlag(mailbox, '\\Seen'), true);
441
+ test.equal(tools.canUseFlag(mailbox, '\\Flagged'), true);
442
+ test.equal(tools.canUseFlag(mailbox, '\\Deleted'), false);
443
+ test.done();
444
+ };
445
+
446
+ module.exports['Tools: canUseFlag with no permanent flags'] = test => {
447
+ let mailbox = { permanentFlags: null };
448
+ test.equal(tools.canUseFlag(mailbox, '\\Seen'), true);
449
+ test.done();
450
+ };
451
+
452
+ // ============================================
453
+ // expandRange tests
454
+ // ============================================
455
+
456
+ module.exports['Tools: expandRange with single values'] = test => {
457
+ let result = tools.expandRange('1,2,3');
458
+ test.deepEqual(result, [1, 2, 3]);
459
+ test.done();
460
+ };
461
+
462
+ module.exports['Tools: expandRange with range'] = test => {
463
+ let result = tools.expandRange('1:5');
464
+ test.deepEqual(result, [1, 2, 3, 4, 5]);
465
+ test.done();
466
+ };
467
+
468
+ module.exports['Tools: expandRange with reverse range'] = test => {
469
+ let result = tools.expandRange('5:1');
470
+ test.deepEqual(result, [5, 4, 3, 2, 1]);
471
+ test.done();
472
+ };
473
+
474
+ module.exports['Tools: expandRange with mixed'] = test => {
475
+ let result = tools.expandRange('1,3:5,10');
476
+ test.deepEqual(result, [1, 3, 4, 5, 10]);
477
+ test.done();
478
+ };
479
+
480
+ module.exports['Tools: expandRange with same start/end'] = test => {
481
+ let result = tools.expandRange('5:5');
482
+ test.deepEqual(result, [5]);
483
+ test.done();
484
+ };
485
+
486
+ // ============================================
487
+ // packMessageRange tests
488
+ // ============================================
489
+
490
+ module.exports['Tools: packMessageRange with sequential numbers'] = test => {
491
+ let result = tools.packMessageRange([1, 2, 3, 4, 5]);
492
+ test.equal(result, '1:5');
493
+ test.done();
494
+ };
495
+
496
+ module.exports['Tools: packMessageRange with gaps'] = test => {
497
+ let result = tools.packMessageRange([1, 2, 3, 7, 8, 9]);
498
+ test.equal(result, '1:3,7:9');
499
+ test.done();
500
+ };
501
+
502
+ module.exports['Tools: packMessageRange with single values'] = test => {
503
+ let result = tools.packMessageRange([1, 5, 10]);
504
+ test.equal(result, '1,5,10');
505
+ test.done();
506
+ };
507
+
508
+ module.exports['Tools: packMessageRange with unsorted input'] = test => {
509
+ let result = tools.packMessageRange([5, 1, 3, 2, 4]);
510
+ test.equal(result, '1:5');
511
+ test.done();
512
+ };
513
+
514
+ module.exports['Tools: packMessageRange with empty array'] = test => {
515
+ test.equal(tools.packMessageRange([]), '');
516
+ test.done();
517
+ };
518
+
519
+ module.exports['Tools: packMessageRange with non-array'] = test => {
520
+ test.equal(tools.packMessageRange(5), '5');
521
+ test.equal(tools.packMessageRange(null), '');
522
+ test.done();
523
+ };
524
+
525
+ // ============================================
526
+ // processName tests
527
+ // ============================================
528
+
529
+ module.exports['Tools: processName with quoted string'] = test => {
530
+ test.equal(tools.processName('"John Doe"'), 'John Doe');
531
+ test.done();
532
+ };
533
+
534
+ module.exports['Tools: processName with unquoted string'] = test => {
535
+ test.equal(tools.processName('John Doe'), 'John Doe');
536
+ test.done();
537
+ };
538
+
539
+ module.exports['Tools: processName with null/undefined'] = test => {
540
+ test.equal(tools.processName(null), '');
541
+ test.equal(tools.processName(undefined), '');
542
+ test.done();
543
+ };
544
+
545
+ module.exports['Tools: processName with short quoted'] = test => {
546
+ // String too short to have quotes removed (less than 3 chars)
547
+ test.equal(tools.processName('""'), '""');
548
+ test.equal(tools.processName('"a"'), 'a');
549
+ test.done();
550
+ };
551
+
552
+ // ============================================
553
+ // getFolderTree tests
554
+ // ============================================
555
+
556
+ module.exports['Tools: getFolderTree with flat folders'] = test => {
557
+ let folders = [
558
+ { name: 'INBOX', path: 'INBOX', flags: new Set(), parent: [] },
559
+ { name: 'Sent', path: 'Sent', flags: new Set(), parent: [] }
560
+ ];
561
+ let tree = tools.getFolderTree(folders);
562
+
563
+ test.ok(tree.root);
564
+ test.ok(Array.isArray(tree.folders));
565
+ test.equal(tree.folders.length, 2);
566
+ test.done();
567
+ };
568
+
569
+ module.exports['Tools: getFolderTree with nested folders'] = test => {
570
+ let folders = [
571
+ { name: 'INBOX', path: 'INBOX', flags: new Set(['\\HasChildren']), parent: [] },
572
+ { name: 'Work', path: 'INBOX/Work', flags: new Set(), parent: ['INBOX'] }
573
+ ];
574
+ let tree = tools.getFolderTree(folders);
575
+
576
+ test.ok(tree.root);
577
+ test.equal(tree.folders.length, 1);
578
+ test.equal(tree.folders[0].name, 'INBOX');
579
+ test.ok(Array.isArray(tree.folders[0].folders));
580
+ test.done();
581
+ };
582
+
583
+ module.exports['Tools: getFolderTree with Noselect flag'] = test => {
584
+ let folders = [{ name: 'Archive', path: 'Archive', flags: new Set(['\\Noselect']), parent: [] }];
585
+ let tree = tools.getFolderTree(folders);
586
+
587
+ test.equal(tree.folders[0].disabled, true);
588
+ test.done();
589
+ };
590
+
591
+ module.exports['Tools: getFolderTree with specialUse'] = test => {
592
+ let folders = [{ name: 'Sent', path: 'Sent', flags: new Set(), parent: [], specialUse: '\\Sent' }];
593
+ let tree = tools.getFolderTree(folders);
594
+
595
+ test.equal(tree.folders[0].specialUse, '\\Sent');
596
+ test.done();
597
+ };
598
+
599
+ module.exports['Tools: getFolderTree with delimiter'] = test => {
600
+ let folders = [{ name: 'Folder', path: 'Folder', flags: new Set(), parent: [], delimiter: '/' }];
601
+ let tree = tools.getFolderTree(folders);
602
+
603
+ test.equal(tree.folders[0].delimiter, '/');
604
+ test.done();
605
+ };
606
+
607
+ module.exports['Tools: getFolderTree updates existing entries'] = test => {
608
+ let folders = [
609
+ { name: 'INBOX', path: 'INBOX', flags: new Set(['\\HasChildren']), parent: [], listed: true },
610
+ { name: 'INBOX', path: 'INBOX', flags: new Set(['\\HasChildren']), parent: [], subscribed: true }
611
+ ];
612
+ let tree = tools.getFolderTree(folders);
613
+
614
+ // Should update the existing entry, not create duplicate
615
+ test.equal(tree.folders.length, 1);
616
+ test.done();
617
+ };
618
+
619
+ // ============================================
620
+ // parseEnvelope tests
621
+ // ============================================
622
+
623
+ module.exports['Tools: parseEnvelope with complete envelope'] = test => {
624
+ let entry = [
625
+ { value: 'Mon, 15 Jun 2023 10:00:00 +0000' }, // date
626
+ { value: 'Test Subject' }, // subject
627
+ [[{ value: 'Sender Name' }, null, { value: 'sender' }, { value: 'example.com' }]], // from
628
+ [[{ value: 'Sender Name' }, null, { value: 'sender' }, { value: 'example.com' }]], // sender
629
+ [[{ value: 'Reply Name' }, null, { value: 'reply' }, { value: 'example.com' }]], // reply-to
630
+ [[{ value: 'To Name' }, null, { value: 'to' }, { value: 'example.com' }]], // to
631
+ [[{ value: 'CC Name' }, null, { value: 'cc' }, { value: 'example.com' }]], // cc
632
+ [[{ value: 'BCC Name' }, null, { value: 'bcc' }, { value: 'example.com' }]], // bcc
633
+ { value: '<reply-id@example.com>' }, // in-reply-to
634
+ { value: '<message-id@example.com>' } // message-id
635
+ ];
636
+
637
+ let result = tools.parseEnvelope(entry);
638
+
639
+ test.ok(result.date instanceof Date);
640
+ test.equal(result.subject, 'Test Subject');
641
+ test.equal(result.from[0].address, 'sender@example.com');
642
+ test.equal(result.to[0].address, 'to@example.com');
643
+ test.equal(result.messageId, '<message-id@example.com>');
644
+ test.done();
645
+ };
646
+
647
+ module.exports['Tools: parseEnvelope with minimal envelope'] = test => {
648
+ let entry = [
649
+ null, // date
650
+ null, // subject
651
+ [], // from
652
+ [], // sender
653
+ [], // reply-to
654
+ [], // to
655
+ [], // cc
656
+ [], // bcc
657
+ null, // in-reply-to
658
+ null // message-id
659
+ ];
660
+
661
+ let result = tools.parseEnvelope(entry);
662
+ test.ok(typeof result === 'object');
663
+ test.equal(result.subject, undefined);
664
+ test.done();
665
+ };
666
+
667
+ module.exports['Tools: parseEnvelope with invalid date'] = test => {
668
+ let entry = [{ value: 'invalid-date' }, null, null, null, null, null, null, null, null, null];
669
+
670
+ let result = tools.parseEnvelope(entry);
671
+ test.equal(result.date, 'invalid-date');
672
+ test.done();
673
+ };
674
+
675
+ module.exports['Tools: parseEnvelope with Buffer value'] = test => {
676
+ let entry = [
677
+ { value: Buffer.from('Mon, 15 Jun 2023 10:00:00 +0000') }, // date as Buffer
678
+ { value: Buffer.from('Buffer Subject') }, // subject as Buffer
679
+ [[{ value: Buffer.from('Sender Name') }, null, { value: Buffer.from('sender') }, { value: Buffer.from('example.com') }]], // from with Buffers
680
+ [], // sender
681
+ [], // reply-to
682
+ [], // to
683
+ [], // cc
684
+ [], // bcc
685
+ null, // in-reply-to
686
+ { value: Buffer.from('<msg-id@example.com>') } // message-id as Buffer
687
+ ];
688
+
689
+ let result = tools.parseEnvelope(entry);
690
+ test.equal(result.subject, 'Buffer Subject');
691
+ test.equal(result.from[0].address, 'sender@example.com');
692
+ test.equal(result.messageId, '<msg-id@example.com>');
693
+ test.done();
694
+ };
695
+
696
+ module.exports['Tools: parseEnvelope with empty address parts'] = test => {
697
+ // When both local part and domain are null/empty, address should be empty string
698
+ let entry = [
699
+ null, // date
700
+ null, // subject
701
+ [[{ value: 'Group Name' }, null, null, null]], // from with no email parts (group syntax)
702
+ [], // sender
703
+ [], // reply-to
704
+ [], // to
705
+ [], // cc
706
+ [], // bcc
707
+ null, // in-reply-to
708
+ null // message-id
709
+ ];
710
+
711
+ let result = tools.parseEnvelope(entry);
712
+ // Address '@' should be converted to empty string and filtered out if no name
713
+ test.ok(result.from);
714
+ // The entry has a name but no valid address, should still be included with empty address
715
+ test.equal(result.from.length, 1);
716
+ test.equal(result.from[0].name, 'Group Name');
717
+ test.equal(result.from[0].address, '');
718
+ test.done();
719
+ };
720
+
721
+ // ============================================
722
+ // getStructuredParams tests
723
+ // ============================================
724
+
725
+ module.exports['Tools: getStructuredParams with simple params'] = test => {
726
+ let arr = [{ value: 'charset' }, { value: 'utf-8' }, { value: 'name' }, { value: 'file.txt' }];
727
+
728
+ let result = tools.getStructuredParams(arr);
729
+ test.equal(result.charset, 'utf-8');
730
+ test.equal(result.name, 'file.txt');
731
+ test.done();
732
+ };
733
+
734
+ module.exports['Tools: getStructuredParams with null'] = test => {
735
+ let result = tools.getStructuredParams(null);
736
+ test.deepEqual(result, {});
737
+ test.done();
738
+ };
739
+
740
+ module.exports['Tools: getStructuredParams with continuation'] = test => {
741
+ // RFC 2231 continuation
742
+ let arr = [{ value: 'filename*0' }, { value: 'very' }, { value: 'filename*1' }, { value: 'long' }, { value: 'filename*2' }, { value: 'name.txt' }];
743
+
744
+ let result = tools.getStructuredParams(arr);
745
+ test.equal(result.filename, 'verylongname.txt');
746
+ test.done();
747
+ };
748
+
749
+ // ============================================
750
+ // parseBodystructure tests
751
+ // ============================================
752
+
753
+ module.exports['Tools: parseBodystructure with simple text'] = test => {
754
+ let entry = [
755
+ { value: 'TEXT' },
756
+ { value: 'PLAIN' },
757
+ [{ value: 'CHARSET' }, { value: 'UTF-8' }],
758
+ null, // id
759
+ null, // description
760
+ { value: '7BIT' }, // encoding
761
+ { value: '1234' }, // size
762
+ { value: '50' } // lines
763
+ ];
764
+
765
+ let result = tools.parseBodystructure(entry);
766
+ test.equal(result.type, 'text/plain');
767
+ test.equal(result.encoding, '7bit');
768
+ test.equal(result.size, 1234);
769
+ test.equal(result.parameters.charset, 'UTF-8');
770
+ test.done();
771
+ };
772
+
773
+ module.exports['Tools: parseBodystructure with multipart'] = test => {
774
+ let textPart = [{ value: 'TEXT' }, { value: 'PLAIN' }, null, null, null, { value: '7BIT' }, { value: '100' }, { value: '5' }];
775
+
776
+ let htmlPart = [{ value: 'TEXT' }, { value: 'HTML' }, null, null, null, { value: 'QUOTED-PRINTABLE' }, { value: '200' }, { value: '10' }];
777
+
778
+ let entry = [textPart, htmlPart, { value: 'ALTERNATIVE' }];
779
+
780
+ let result = tools.parseBodystructure(entry);
781
+ test.equal(result.type, 'multipart/alternative');
782
+ test.ok(Array.isArray(result.childNodes));
783
+ test.equal(result.childNodes.length, 2);
784
+ test.equal(result.childNodes[0].type, 'text/plain');
785
+ test.equal(result.childNodes[1].type, 'text/html');
786
+ test.done();
787
+ };
788
+
789
+ module.exports['Tools: parseBodystructure with attachment'] = test => {
790
+ let entry = [
791
+ { value: 'APPLICATION' },
792
+ { value: 'PDF' },
793
+ [{ value: 'NAME' }, { value: 'document.pdf' }],
794
+ null,
795
+ null,
796
+ { value: 'BASE64' },
797
+ { value: '50000' }
798
+ ];
799
+
800
+ let result = tools.parseBodystructure(entry);
801
+ test.equal(result.type, 'application/pdf');
802
+ test.equal(result.parameters.name, 'document.pdf');
803
+ test.done();
804
+ };
805
+
806
+ module.exports['Tools: parseBodystructure with md5'] = test => {
807
+ // Non-text type with extension data including md5
808
+ let entry = [
809
+ { value: 'APPLICATION' },
810
+ { value: 'OCTET-STREAM' },
811
+ null, // params
812
+ null, // id
813
+ null, // description
814
+ { value: 'BASE64' }, // encoding
815
+ { value: '1000' }, // size
816
+ { value: 'd41d8cd98f00b204e9800998ecf8427e' }, // md5
817
+ null, // disposition
818
+ null // language (to ensure we have enough elements)
819
+ ];
820
+
821
+ let result = tools.parseBodystructure(entry);
822
+ test.equal(result.type, 'application/octet-stream');
823
+ test.equal(result.md5, 'd41d8cd98f00b204e9800998ecf8427e');
824
+ test.done();
825
+ };
826
+
827
+ module.exports['Tools: parseBodystructure with language'] = test => {
828
+ // Non-text type with language extension
829
+ let entry = [
830
+ { value: 'APPLICATION' },
831
+ { value: 'PDF' },
832
+ null, // params
833
+ null, // id
834
+ null, // description
835
+ { value: 'BASE64' }, // encoding
836
+ { value: '5000' }, // size
837
+ null, // md5
838
+ null, // disposition
839
+ [{ value: 'EN' }, { value: 'DE' }], // language (array of values)
840
+ null // location (to ensure enough elements)
841
+ ];
842
+
843
+ let result = tools.parseBodystructure(entry);
844
+ test.equal(result.type, 'application/pdf');
845
+ test.ok(Array.isArray(result.language));
846
+ test.deepEqual(result.language, ['en', 'de']);
847
+ test.done();
848
+ };
849
+
850
+ module.exports['Tools: parseBodystructure with location'] = test => {
851
+ // Non-text type with location extension
852
+ let entry = [
853
+ { value: 'IMAGE' },
854
+ { value: 'PNG' },
855
+ null, // params
856
+ null, // id
857
+ null, // description
858
+ { value: 'BASE64' }, // encoding
859
+ { value: '10000' }, // size
860
+ null, // md5
861
+ null, // disposition
862
+ null, // language
863
+ { value: 'http://example.com/image.png' }, // location
864
+ null // extra element to ensure we have enough
865
+ ];
866
+
867
+ let result = tools.parseBodystructure(entry);
868
+ test.equal(result.type, 'image/png');
869
+ test.equal(result.location, 'http://example.com/image.png');
870
+ test.done();
871
+ };
872
+
873
+ module.exports['Tools: parseBodystructure with all extension fields'] = test => {
874
+ // Non-text type with all extension fields
875
+ let entry = [
876
+ { value: 'APPLICATION' },
877
+ { value: 'ZIP' },
878
+ [{ value: 'NAME' }, { value: 'archive.zip' }], // params
879
+ { value: '<id123@example.com>' }, // id
880
+ { value: 'A zip archive' }, // description
881
+ { value: 'BASE64' }, // encoding
882
+ { value: '50000' }, // size
883
+ { value: 'abc123def456' }, // md5
884
+ [{ value: 'ATTACHMENT' }, [{ value: 'FILENAME' }, { value: 'archive.zip' }]], // disposition with params
885
+ [{ value: 'EN' }], // language
886
+ { value: 'http://example.com/archive.zip' }, // location
887
+ null // extra element
888
+ ];
889
+
890
+ let result = tools.parseBodystructure(entry);
891
+ test.equal(result.type, 'application/zip');
892
+ test.equal(result.parameters.name, 'archive.zip');
893
+ test.equal(result.id, '<id123@example.com>');
894
+ test.equal(result.description, 'A zip archive');
895
+ test.equal(result.encoding, 'base64');
896
+ test.equal(result.size, 50000);
897
+ test.equal(result.md5, 'abc123def456');
898
+ test.equal(result.disposition, 'attachment');
899
+ test.equal(result.dispositionParameters.filename, 'archive.zip');
900
+ test.deepEqual(result.language, ['en']);
901
+ test.equal(result.location, 'http://example.com/archive.zip');
902
+ test.done();
903
+ };
904
+
905
+ module.exports['Tools: parseBodystructure with message/rfc822'] = test => {
906
+ // message/rfc822 has special handling with envelope and nested bodystructure
907
+ let nestedBody = [
908
+ { value: 'TEXT' },
909
+ { value: 'PLAIN' },
910
+ [{ value: 'CHARSET' }, { value: 'UTF-8' }],
911
+ null,
912
+ null,
913
+ { value: '7BIT' },
914
+ { value: '500' },
915
+ { value: '20' } // line count for text
916
+ ];
917
+
918
+ let envelope = [
919
+ { value: 'Mon, 15 Jun 2023 10:00:00 +0000' }, // date
920
+ { value: 'Nested Subject' }, // subject
921
+ [[null, null, { value: 'sender' }, { value: 'example.com' }]], // from
922
+ [],
923
+ [],
924
+ [],
925
+ [],
926
+ [], // sender, reply-to, to, cc, bcc
927
+ null, // in-reply-to
928
+ { value: '<nested@example.com>' } // message-id
929
+ ];
930
+
931
+ let entry = [
932
+ { value: 'MESSAGE' },
933
+ { value: 'RFC822' },
934
+ null, // params
935
+ null, // id
936
+ null, // description
937
+ { value: '7BIT' }, // encoding
938
+ { value: '10000' }, // size
939
+ envelope, // envelope
940
+ nestedBody, // nested bodystructure
941
+ { value: '100' }, // line count
942
+ null, // md5
943
+ null // disposition
944
+ ];
945
+
946
+ let result = tools.parseBodystructure(entry);
947
+ test.equal(result.type, 'message/rfc822');
948
+ test.equal(result.size, 10000);
949
+ test.equal(result.lineCount, 100);
950
+ test.ok(result.envelope);
951
+ test.equal(result.envelope.subject, 'Nested Subject');
952
+ test.ok(result.childNodes);
953
+ test.equal(result.childNodes.length, 1);
954
+ test.equal(result.childNodes[0].type, 'text/plain');
955
+ test.done();
956
+ };
957
+
958
+ // ============================================
959
+ // getDecoder tests
960
+ // ============================================
961
+
962
+ module.exports['Tools: getDecoder with standard charset'] = test => {
963
+ let decoder = tools.getDecoder('utf-8');
964
+ test.ok(decoder);
965
+ test.ok(typeof decoder.write === 'function');
966
+ test.done();
967
+ };
968
+
969
+ module.exports['Tools: getDecoder with Japanese charset'] = test => {
970
+ let decoder = tools.getDecoder('iso-2022-jp');
971
+ test.ok(decoder);
972
+ test.equal(decoder.constructor.name, 'JPDecoder');
973
+ test.done();
974
+ };
975
+
976
+ module.exports['Tools: getDecoder with null/undefined'] = test => {
977
+ let decoder = tools.getDecoder(null);
978
+ test.ok(decoder);
979
+ test.done();
980
+ };
981
+
982
+ // ============================================
983
+ // AuthenticationFailure tests
984
+ // ============================================
985
+
986
+ module.exports['Tools: AuthenticationFailure error class'] = test => {
987
+ let error = new tools.AuthenticationFailure('Auth failed');
988
+ test.ok(error instanceof Error);
989
+ test.equal(error.authenticationFailed, true);
990
+ test.equal(error.message, 'Auth failed');
991
+ test.done();
992
+ };