imapflow 1.3.2 → 1.3.3

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.3.2"
2
+ ".": "1.3.3"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.3](https://github.com/postalsys/imapflow/compare/v1.3.2...v1.3.3) (2026-04-29)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * handle NIL delimiter in NAMESPACE response (RFC 2342) ([#351](https://github.com/postalsys/imapflow/issues/351)) ([10bda2a](https://github.com/postalsys/imapflow/commit/10bda2a1d398dc7bc6ea0ede50b1ae48e4b28cf7))
9
+
3
10
  ## [1.3.2](https://github.com/postalsys/imapflow/compare/v1.3.1...v1.3.2) (2026-04-17)
4
11
 
5
12
 
@@ -4,7 +4,7 @@
4
4
  * Requests NAMESPACE info from the server.
5
5
  *
6
6
  * @param {Object} connection - IMAP connection instance
7
- * @returns {Promise<{prefix: string, delimiter: string}|{error: boolean, status: string, text: string}>} The primary personal namespace, or an error object on failure
7
+ * @returns {Promise<{prefix: string, delimiter: string|null}|{error: boolean, status: string, text: string}>} The primary personal namespace, or an error object on failure
8
8
  */
9
9
  module.exports = async connection => {
10
10
  if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
@@ -114,7 +114,7 @@ async function getListPrefix(connection) {
114
114
  * Parses namespace information from an IMAP NAMESPACE response attribute.
115
115
  *
116
116
  * @param {Array} attribute - Namespace attribute array from the server response
117
- * @returns {Array<{prefix: string, delimiter: string}>|boolean} Array of namespace entries, or false if empty
117
+ * @returns {Array<{prefix: string, delimiter: string|null}>|boolean} Array of namespace entries, or false if empty
118
118
  */
119
119
  function getNamsepaceInfo(attribute) {
120
120
  if (!attribute || !attribute.length) {
@@ -122,10 +122,15 @@ function getNamsepaceInfo(attribute) {
122
122
  }
123
123
 
124
124
  return attribute
125
- .filter(entry => entry.length >= 2 && typeof entry[0].value === 'string' && typeof entry[1].value === 'string')
125
+ .filter(
126
+ entry =>
127
+ // RFC 2342 §5 allows the delimiter to be NIL when the namespace has
128
+ // no hierarchy. The token parser emits a literal `null` for NIL.
129
+ entry.length >= 2 && entry[0] && typeof entry[0].value === 'string' && (entry[1] === null || (entry[1] && typeof entry[1].value === 'string'))
130
+ )
126
131
  .map(entry => {
127
132
  let prefix = entry[0].value;
128
- let delimiter = entry[1].value;
133
+ let delimiter = entry[1] === null ? null : entry[1].value;
129
134
 
130
135
  // Append the delimiter to the prefix if it doesn't already end with one,
131
136
  // so callers can construct full paths by simply concatenating prefix + name.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "lib/imap-flow.js",
6
6
  "types": "lib/imap-flow.d.ts",
@@ -30,7 +30,7 @@
30
30
  "@eslint/js": "10.0.1",
31
31
  "@types/node": "25.6.0",
32
32
  "c8": "11.0.0",
33
- "eslint": "10.2.0",
33
+ "eslint": "10.2.1",
34
34
  "eslint-config-nodemailer": "1.2.0",
35
35
  "eslint-config-prettier": "10.1.8",
36
36
  "grunt": "1.6.2",
@@ -48,8 +48,8 @@
48
48
  "libbase64": "1.3.0",
49
49
  "libmime": "5.3.8",
50
50
  "libqp": "2.1.1",
51
- "nodemailer": "8.0.5",
51
+ "nodemailer": "8.0.7",
52
52
  "pino": "10.3.1",
53
- "socks": "2.8.7"
53
+ "socks": "2.8.8"
54
54
  }
55
55
  }
@@ -6457,6 +6457,185 @@ module.exports['Commands: namespace handles NIL namespaces'] = async test => {
6457
6457
  test.done();
6458
6458
  };
6459
6459
 
6460
+ module.exports['Commands: namespace handles NIL delimiter (RFC 2342)'] = async test => {
6461
+ // RFC 2342 §5: NIL delimiter means the namespace has no hierarchy.
6462
+ // The token parser emits a literal `null` for NIL.
6463
+
6464
+ const connection = createMockConnection({
6465
+ state: 2,
6466
+ capabilities: new Map([['NAMESPACE', true]]),
6467
+ exec: async (cmd, args, opts) => {
6468
+ if (opts && opts.untagged && opts.untagged.NAMESPACE) {
6469
+ await opts.untagged.NAMESPACE({
6470
+ attributes: [
6471
+ [
6472
+ [{ value: '' }, { value: '/' }],
6473
+ [{ value: '#hidden' }, null]
6474
+ ],
6475
+ null,
6476
+ null
6477
+ ]
6478
+ });
6479
+ }
6480
+ return { next: () => {} };
6481
+ }
6482
+ });
6483
+
6484
+ const result = await namespaceCommand(connection);
6485
+
6486
+ // Guard against handler crash so a regression surfaces as a failed
6487
+ // assertion rather than fatal-halting the whole nodeunit suite.
6488
+ if (result && result.error) {
6489
+ test.ok(false, 'namespace command returned error sentinel — likely crashed on NIL delimiter');
6490
+ test.done();
6491
+ return;
6492
+ }
6493
+ if (!Array.isArray(connection.namespaces && connection.namespaces.personal)) {
6494
+ test.ok(false, 'connection.namespaces.personal is not an array (handler crashed mid-untagged-callback)');
6495
+ test.done();
6496
+ return;
6497
+ }
6498
+
6499
+ test.equal(connection.namespaces.personal.length, 2, 'both personal entries should be parsed');
6500
+
6501
+ test.equal(connection.namespaces.personal[0].prefix, '');
6502
+ test.equal(connection.namespaces.personal[0].delimiter, '/');
6503
+
6504
+ // NIL-delimiter entry preserved with delimiter:null rather than dropped silently.
6505
+ test.equal(connection.namespaces.personal[1].prefix, '#hidden');
6506
+ test.equal(connection.namespaces.personal[1].delimiter, null);
6507
+
6508
+ // Default namespace pointer resolves to the first valid personal entry.
6509
+ test.equal(connection.namespace.prefix, '');
6510
+ test.equal(connection.namespace.delimiter, '/');
6511
+
6512
+ test.done();
6513
+ };
6514
+
6515
+ module.exports['Commands: namespace handles NIL delimiter as only personal entry'] = async test => {
6516
+ // Edge case: only a NIL-delimiter entry in the personal section.
6517
+ // connection.namespace must still be set and usable downstream.
6518
+
6519
+ const connection = createMockConnection({
6520
+ state: 2,
6521
+ capabilities: new Map([['NAMESPACE', true]]),
6522
+ exec: async (cmd, args, opts) => {
6523
+ if (opts && opts.untagged && opts.untagged.NAMESPACE) {
6524
+ await opts.untagged.NAMESPACE({
6525
+ attributes: [[[{ value: '#hidden' }, null]], null, null]
6526
+ });
6527
+ }
6528
+ return { next: () => {} };
6529
+ }
6530
+ });
6531
+
6532
+ const result = await namespaceCommand(connection);
6533
+
6534
+ if (result && result.error) {
6535
+ test.ok(false, 'namespace command returned error sentinel — likely crashed on NIL delimiter');
6536
+ test.done();
6537
+ return;
6538
+ }
6539
+ if (!Array.isArray(connection.namespaces && connection.namespaces.personal)) {
6540
+ test.ok(false, 'connection.namespaces.personal is not an array (handler crashed mid-untagged-callback)');
6541
+ test.done();
6542
+ return;
6543
+ }
6544
+
6545
+ test.equal(connection.namespaces.personal.length, 1);
6546
+ test.equal(connection.namespaces.personal[0].prefix, '#hidden');
6547
+ test.equal(connection.namespaces.personal[0].delimiter, null);
6548
+
6549
+ test.ok(connection.namespace);
6550
+ test.equal(connection.namespace.prefix, '#hidden');
6551
+ test.equal(connection.namespace.delimiter, null);
6552
+
6553
+ test.done();
6554
+ };
6555
+
6556
+ module.exports['Commands: namespace handles NIL delimiter in other and shared sections'] = async test => {
6557
+ // NIL delimiter is also valid in the `other` and `shared` sections,
6558
+ // not just `personal`.
6559
+
6560
+ const connection = createMockConnection({
6561
+ state: 2,
6562
+ capabilities: new Map([['NAMESPACE', true]]),
6563
+ exec: async (cmd, args, opts) => {
6564
+ if (opts && opts.untagged && opts.untagged.NAMESPACE) {
6565
+ await opts.untagged.NAMESPACE({
6566
+ attributes: [[[{ value: '' }, { value: '/' }]], [[{ value: 'Other Users/' }, null]], [[{ value: 'Public/' }, null]]]
6567
+ });
6568
+ }
6569
+ return { next: () => {} };
6570
+ }
6571
+ });
6572
+
6573
+ const result = await namespaceCommand(connection);
6574
+
6575
+ if (result && result.error) {
6576
+ test.ok(false, 'namespace command returned error sentinel — likely crashed on NIL delimiter');
6577
+ test.done();
6578
+ return;
6579
+ }
6580
+
6581
+ test.ok(Array.isArray(connection.namespaces.other), 'other should be an array');
6582
+ test.equal(connection.namespaces.other.length, 1);
6583
+ test.equal(connection.namespaces.other[0].prefix, 'Other Users/');
6584
+ test.equal(connection.namespaces.other[0].delimiter, null);
6585
+
6586
+ test.ok(Array.isArray(connection.namespaces.shared), 'shared should be an array');
6587
+ test.equal(connection.namespaces.shared.length, 1);
6588
+ test.equal(connection.namespaces.shared[0].prefix, 'Public/');
6589
+ test.equal(connection.namespaces.shared[0].delimiter, null);
6590
+
6591
+ test.done();
6592
+ };
6593
+
6594
+ module.exports['Commands: namespace skips malformed entries without crashing'] = async test => {
6595
+ // The filter must reject entries that don't match either the
6596
+ // (string-prefix, string-delimiter) or (string-prefix, NIL) shape,
6597
+ // so a single broken entry from a buggy server doesn't poison
6598
+ // the whole namespace list or crash the handler.
6599
+
6600
+ const connection = createMockConnection({
6601
+ state: 2,
6602
+ capabilities: new Map([['NAMESPACE', true]]),
6603
+ exec: async (cmd, args, opts) => {
6604
+ if (opts && opts.untagged && opts.untagged.NAMESPACE) {
6605
+ await opts.untagged.NAMESPACE({
6606
+ attributes: [
6607
+ [
6608
+ [{ value: '' }, { value: '/' }], // valid
6609
+ [{ value: '#hidden' }, null], // valid (NIL delimiter)
6610
+ [{ value: 'broken' }], // too short — rejected
6611
+ [null, { value: '/' }], // null prefix — rejected
6612
+ [{ value: 123 }, { value: '/' }] // non-string prefix — rejected
6613
+ ],
6614
+ null,
6615
+ null
6616
+ ]
6617
+ });
6618
+ }
6619
+ return { next: () => {} };
6620
+ }
6621
+ });
6622
+
6623
+ const result = await namespaceCommand(connection);
6624
+
6625
+ if (result && result.error) {
6626
+ test.ok(false, 'namespace command returned error sentinel — likely crashed on malformed entry');
6627
+ test.done();
6628
+ return;
6629
+ }
6630
+
6631
+ test.equal(connection.namespaces.personal.length, 2, 'only the two well-formed entries should be kept');
6632
+ test.equal(connection.namespaces.personal[0].prefix, '');
6633
+ test.equal(connection.namespaces.personal[1].prefix, '#hidden');
6634
+ test.equal(connection.namespaces.personal[1].delimiter, null);
6635
+
6636
+ test.done();
6637
+ };
6638
+
6460
6639
  module.exports['Commands: namespace handles multiple personal namespaces'] = async test => {
6461
6640
  const connection = createMockConnection({
6462
6641
  state: 2,