imapflow 1.4.6 → 1.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.4.6"
2
+ ".": "1.4.7"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.7](https://github.com/postalsys/imapflow/compare/v1.4.6...v1.4.7) (2026-07-10)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * treat \NonExistent mailboxes as \Noselect in LIST responses ([af21245](https://github.com/postalsys/imapflow/commit/af21245dab4e1320afc4c4458afe33d1508180c1))
9
+
3
10
  ## [1.4.6](https://github.com/postalsys/imapflow/compare/v1.4.5...v1.4.6) (2026-07-05)
4
11
 
5
12
 
@@ -87,6 +87,15 @@ module.exports = async (connection, reference, mailbox, options) => {
87
87
  specialUseMatches[type].push({ entry, source });
88
88
  };
89
89
 
90
+ // RFC 5258: the \NonExistent attribute implies \Noselect. Some servers only
91
+ // return \NonExistent for phantom folders, so add \Noselect as well to keep
92
+ // the flags consistent for consumers that only check \Noselect.
93
+ let normalizeFlags = entry => {
94
+ if (entry.flags.has('\\NonExistent')) {
95
+ entry.flags.add('\\Noselect');
96
+ }
97
+ };
98
+
90
99
  // User-provided hints map mailbox paths to special-use types (e.g., {sent: "Sent Items"}).
91
100
  // These override server-reported flags and name-based guesses. Converted to a
92
101
  // path-keyed lookup: { "Sent Items" => "\\Sent" }
@@ -132,6 +141,8 @@ module.exports = async (connection, reference, mailbox, options) => {
132
141
  listed: true
133
142
  };
134
143
 
144
+ normalizeFlags(entry);
145
+
135
146
  // Check user-provided hints first (highest priority)
136
147
  if (specialUseHints[entry.path]) {
137
148
  addSpecialUseMatch(entry, specialUseHints[entry.path], 'user');
@@ -300,6 +311,7 @@ module.exports = async (connection, reference, mailbox, options) => {
300
311
  existing.subscribed = true;
301
312
  // Merge any additional flags from LSUB into the LIST entry
302
313
  entry.flags.forEach(flag => existing.flags.add(flag));
314
+ normalizeFlags(existing);
303
315
  }
304
316
  // Non-listed subscribed folders are intentionally ignored
305
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.4.6",
3
+ "version": "1.4.7",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "lib/imap-flow.js",
6
6
  "types": "lib/imap-flow.d.ts",
@@ -3550,6 +3550,94 @@ module.exports['Commands: list skips Noselect folders for status'] = async test
3550
3550
  test.done();
3551
3551
  };
3552
3552
 
3553
+ module.exports['Commands: list adds Noselect to NonExistent mailboxes'] = async test => {
3554
+ const connection = createMockConnection({
3555
+ state: 3,
3556
+ exec: async (cmd, attrs, opts) => {
3557
+ if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
3558
+ await opts.untagged.LIST({
3559
+ attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
3560
+ });
3561
+ await opts.untagged.LIST({
3562
+ attributes: [[{ value: '\\NonExistent' }], { value: '/' }, { value: 'Phantom' }]
3563
+ });
3564
+ }
3565
+ return { next: () => {} };
3566
+ }
3567
+ });
3568
+
3569
+ const result = await listCommand(connection, '', '*');
3570
+ const phantom = result.find(e => e.path === 'Phantom');
3571
+ test.ok(phantom);
3572
+ // RFC 5258: \\NonExistent implies \\Noselect
3573
+ test.equal(phantom.flags.has('\\Noselect'), true);
3574
+ // The original flag is preserved, not replaced
3575
+ test.equal(phantom.flags.has('\\NonExistent'), true);
3576
+ const inbox = result.find(e => e.path === 'INBOX');
3577
+ test.ok(inbox);
3578
+ test.equal(inbox.flags.has('\\Noselect'), false);
3579
+ test.done();
3580
+ };
3581
+
3582
+ module.exports['Commands: list LSUB merge adds Noselect to NonExistent'] = async test => {
3583
+ const connection = createMockConnection({
3584
+ state: 3,
3585
+ exec: async (cmd, attrs, opts) => {
3586
+ if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
3587
+ await opts.untagged.LIST({
3588
+ attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'Folder1' }]
3589
+ });
3590
+ }
3591
+ if (cmd === 'LSUB' && opts && opts.untagged && opts.untagged.LSUB) {
3592
+ // Some servers only report \\NonExistent in LSUB responses
3593
+ await opts.untagged.LSUB({
3594
+ attributes: [[{ value: '\\NonExistent' }], { value: '/' }, { value: 'Folder1' }]
3595
+ });
3596
+ }
3597
+ return { next: () => {} };
3598
+ }
3599
+ });
3600
+
3601
+ const result = await listCommand(connection, '', '*');
3602
+ const folder = result.find(e => e.path === 'Folder1');
3603
+ test.ok(folder);
3604
+ test.equal(folder.subscribed, true);
3605
+ test.equal(folder.flags.has('\\NonExistent'), true);
3606
+ // RFC 5258: \\NonExistent merged from LSUB implies \\Noselect
3607
+ test.equal(folder.flags.has('\\Noselect'), true);
3608
+ test.done();
3609
+ };
3610
+
3611
+ module.exports['Commands: list does not STATUS NonExistent mailboxes'] = async test => {
3612
+ let statusPaths = [];
3613
+ const connection = createMockConnection({
3614
+ state: 3,
3615
+ capabilities: new Map(),
3616
+ run: async (cmd, path) => {
3617
+ if (cmd === 'STATUS') {
3618
+ statusPaths.push(path);
3619
+ return { messages: 10 };
3620
+ }
3621
+ },
3622
+ exec: async (cmd, attrs, opts) => {
3623
+ if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
3624
+ await opts.untagged.LIST({
3625
+ attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
3626
+ });
3627
+ await opts.untagged.LIST({
3628
+ attributes: [[{ value: '\\NonExistent' }], { value: '/' }, { value: 'Phantom' }]
3629
+ });
3630
+ }
3631
+ return { next: () => {} };
3632
+ }
3633
+ });
3634
+
3635
+ await listCommand(connection, '', '*', { statusQuery: { messages: true } });
3636
+ // STATUS runs for the selectable mailbox only
3637
+ test.deepEqual(statusPaths, ['INBOX']);
3638
+ test.done();
3639
+ };
3640
+
3553
3641
  module.exports['Commands: list XLIST removes Inbox flag from non-INBOX'] = async test => {
3554
3642
  const connection = createMockConnection({
3555
3643
  state: 3,