imapflow 1.2.15 → 1.2.17

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.2.15"
2
+ ".": "1.2.17"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.17](https://github.com/postalsys/imapflow/compare/v1.2.16...v1.2.17) (2026-03-25)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * Bumped deps ([e3c75ab](https://github.com/postalsys/imapflow/commit/e3c75ab018b2e4a5f71c4c611ef795ea63539676))
9
+
10
+ ## [1.2.16](https://github.com/postalsys/imapflow/compare/v1.2.15...v1.2.16) (2026-03-19)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * parenthesize compound OR/NOT search conditions (RFC 3501) ([347f668](https://github.com/postalsys/imapflow/commit/347f66827ea089acab88aa78485752d84b7b7f3d)), closes [#106](https://github.com/postalsys/imapflow/issues/106)
16
+
3
17
  ## [1.2.15](https://github.com/postalsys/imapflow/compare/v1.2.14...v1.2.15) (2026-03-16)
4
18
 
5
19
 
@@ -142,6 +142,17 @@ module.exports.searchCompiler = (connection, query) => {
142
142
  * @param {Object} params - Query parameters to process
143
143
  */
144
144
  const walk = params => {
145
+ // Walks a query object and wraps the resulting attributes in a
146
+ // sub-array so the IMAP compiler emits parentheses around them.
147
+ // Used when a single search-key is required (NOT, OR operands)
148
+ // but the condition has multiple keys (RFC 3501 Section 6.4.4).
149
+ let walkGrouped = obj => {
150
+ let startIdx = attributes.length;
151
+ walk(obj);
152
+ let subAttrs = attributes.splice(startIdx);
153
+ attributes.push(subAttrs);
154
+ };
155
+
145
156
  Object.keys(params || {}).forEach(term => {
146
157
  switch (term.toUpperCase()) {
147
158
  // Custom sequence range support (non-standard)
@@ -318,8 +329,11 @@ module.exports.searchCompiler = (connection, query) => {
318
329
  case 'NOT':
319
330
  if (params[term] && typeof params[term] === 'object') {
320
331
  attributes.push({ type: 'ATOM', value: 'NOT' });
321
- // Recursively process NOT conditions
322
- walk(params[term]);
332
+ if (Object.keys(params[term]).length > 1) {
333
+ walkGrouped(params[term]);
334
+ } else {
335
+ walk(params[term]);
336
+ }
323
337
  }
324
338
  break;
325
339
 
@@ -388,7 +402,6 @@ module.exports.searchCompiler = (connection, query) => {
388
402
  */
389
403
  let walkOrTree = entry => {
390
404
  if (Array.isArray(entry)) {
391
- // Only add OR for multiple items
392
405
  if (entry.length > 1) {
393
406
  attributes.push({ type: 'ATOM', value: 'OR' });
394
407
  }
@@ -396,7 +409,11 @@ module.exports.searchCompiler = (connection, query) => {
396
409
  return;
397
410
  }
398
411
  if (entry && typeof entry === 'object') {
399
- walk(entry);
412
+ if (Object.keys(entry).length > 1) {
413
+ walkGrouped(entry);
414
+ } else {
415
+ walk(entry);
416
+ }
400
417
  }
401
418
  };
402
419
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.2.15",
3
+ "version": "1.2.17",
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.5.0",
32
32
  "c8": "11.0.0",
33
- "eslint": "10.0.3",
33
+ "eslint": "10.1.0",
34
34
  "eslint-config-nodemailer": "1.2.0",
35
35
  "eslint-config-prettier": "10.1.8",
36
36
  "grunt": "1.6.1",
@@ -39,7 +39,7 @@
39
39
  "grunt-eslint": "26.0.0",
40
40
  "prettier": "3.8.1",
41
41
  "proxyquire": "^2.1.3",
42
- "typescript": "5.9.3"
42
+ "typescript": "6.0.2"
43
43
  },
44
44
  "dependencies": {
45
45
  "@zone-eu/mailsplit": "5.4.8",
@@ -48,7 +48,7 @@
48
48
  "libbase64": "1.3.0",
49
49
  "libmime": "5.3.7",
50
50
  "libqp": "2.1.1",
51
- "nodemailer": "8.0.2",
51
+ "nodemailer": "8.0.4",
52
52
  "pino": "10.3.1",
53
53
  "socks": "2.8.7"
54
54
  }
@@ -15,9 +15,19 @@ let createMockConnection = (options = {}) => ({
15
15
  mailbox: options.mailbox || createMockMailbox()
16
16
  });
17
17
 
18
- // Helper to find attribute by value
19
- let findAttr = (attrs, value) => attrs.find(a => a.value === value);
20
- let hasAttr = (attrs, value) => attrs.some(a => a.value === value);
18
+ // Helper to find attribute by value (recurses into sub-arrays for parenthesized groups)
19
+ let findAttr = (attrs, value) => {
20
+ for (let a of attrs) {
21
+ if (Array.isArray(a)) {
22
+ let found = findAttr(a, value);
23
+ if (found) return found;
24
+ } else if (a.value === value) {
25
+ return a;
26
+ }
27
+ }
28
+ return undefined;
29
+ };
30
+ let hasAttr = (attrs, value) => !!findAttr(attrs, value);
21
31
 
22
32
  // ============================================
23
33
  // Basic functionality tests
@@ -659,6 +669,10 @@ module.exports['Search Compiler: NOT with nested conditions'] = test => {
659
669
  test.ok(hasAttr(compiled, 'NOT'));
660
670
  test.ok(hasAttr(compiled, 'SEEN'));
661
671
  test.ok(hasAttr(compiled, 'FROM'));
672
+ // Compound NOT conditions should be wrapped in a sub-array (parenthesized)
673
+ // so the server treats them as a single search-key
674
+ test.equal(compiled[0].value, 'NOT');
675
+ test.ok(Array.isArray(compiled[1]), 'compound NOT should be parenthesized');
662
676
  test.done();
663
677
  };
664
678
 
@@ -899,6 +913,70 @@ module.exports['Search Compiler: OR with 5 conditions produces 4 OR atoms'] = te
899
913
  test.done();
900
914
  };
901
915
 
916
+ module.exports['Search Compiler: OR with compound conditions wraps in parentheses'] = test => {
917
+ let connection = createMockConnection();
918
+ let compiled = searchCompiler(connection, {
919
+ or: [
920
+ { to: 'a@example.com', from: 'b@example.com' },
921
+ { to: 'c@example.com', from: 'd@example.com' }
922
+ ]
923
+ });
924
+
925
+ // OR should be present
926
+ test.equal(compiled[0].value, 'OR');
927
+ // Each compound condition should be a sub-array (parenthesized)
928
+ test.ok(Array.isArray(compiled[1]), 'first compound operand should be parenthesized');
929
+ test.ok(Array.isArray(compiled[2]), 'second compound operand should be parenthesized');
930
+ // Check contents of parenthesized groups
931
+ test.ok(hasAttr(compiled[1], 'TO'), 'first group should have TO');
932
+ test.ok(hasAttr(compiled[1], 'FROM'), 'first group should have FROM');
933
+ test.ok(hasAttr(compiled[2], 'TO'), 'second group should have TO');
934
+ test.ok(hasAttr(compiled[2], 'FROM'), 'second group should have FROM');
935
+ test.done();
936
+ };
937
+
938
+ module.exports['Search Compiler: OR with single-key conditions stays flat'] = test => {
939
+ let connection = createMockConnection();
940
+ let compiled = searchCompiler(connection, {
941
+ or: [{ from: 'a@example.com' }, { from: 'b@example.com' }]
942
+ });
943
+
944
+ // Single-key conditions should not be wrapped in sub-arrays
945
+ test.equal(compiled[0].value, 'OR');
946
+ test.ok(!Array.isArray(compiled[1]), 'single-key operand should not be parenthesized');
947
+ test.equal(compiled[1].value, 'FROM');
948
+ test.done();
949
+ };
950
+
951
+ module.exports['Search Compiler: OR with 5 compound conditions from issue #106'] = test => {
952
+ let connection = createMockConnection();
953
+ let compiled = searchCompiler(connection, {
954
+ or: [
955
+ { to: 'myemail@domain.com', from: '@anotherdomain.com' },
956
+ { to: 'myemail@domain.com', from: '@aseconddomain.com' },
957
+ { to: 'myemail@domain.com', from: '@athirddomain.fr' },
958
+ { to: 'anotheremail@domain.com', from: '@anotherdomain.fr' },
959
+ { to: 'anotheremail@domain.com', from: '@aseconddomain.com' }
960
+ ]
961
+ });
962
+
963
+ // 5 conditions need 4 OR atoms
964
+ let orCount = compiled.filter(a => a.value === 'OR').length;
965
+ test.equal(orCount, 4, 'should have exactly 4 OR atoms for 5 conditions');
966
+
967
+ // All compound conditions should be parenthesized (sub-arrays)
968
+ let subArrays = compiled.filter(a => Array.isArray(a));
969
+ test.equal(subArrays.length, 5, 'should have 5 parenthesized groups');
970
+
971
+ // Each group should contain both TO and FROM
972
+ subArrays.forEach((group, i) => {
973
+ test.ok(hasAttr(group, 'TO'), 'group ' + i + ' should have TO');
974
+ test.ok(hasAttr(group, 'FROM'), 'group ' + i + ' should have FROM');
975
+ });
976
+
977
+ test.done();
978
+ };
979
+
902
980
  // ============================================
903
981
  // Unicode CHARSET in HEADER searches
904
982
  // ============================================