imapflow 1.2.15 → 1.2.16
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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/lib/search-compiler.js +21 -4
- package/package.json +2 -2
- package/test/search-compiler-test.js +81 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.16](https://github.com/postalsys/imapflow/compare/v1.2.15...v1.2.16) (2026-03-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* 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)
|
|
9
|
+
|
|
3
10
|
## [1.2.15](https://github.com/postalsys/imapflow/compare/v1.2.14...v1.2.15) (2026-03-16)
|
|
4
11
|
|
|
5
12
|
|
package/lib/search-compiler.js
CHANGED
|
@@ -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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.2.16",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "lib/imap-flow.js",
|
|
6
6
|
"types": "lib/imap-flow.d.ts",
|
|
@@ -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.
|
|
51
|
+
"nodemailer": "8.0.3",
|
|
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) =>
|
|
20
|
-
let
|
|
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
|
// ============================================
|