imapflow 1.7.3 → 1.7.4
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 +23 -6
- package/package.json +1 -1
- package/test/search-compiler-test.js +51 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.4](https://github.com/postalsys/imapflow/compare/v1.7.3...v1.7.4) (2026-08-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **search:** normalize search dates so invalid values cannot throw or emit NaN ([bf6c6bb](https://github.com/postalsys/imapflow/commit/bf6c6bb9c9e649ac9c28eba2c42f744b7d7d8e63)), closes [#385](https://github.com/postalsys/imapflow/issues/385)
|
|
9
|
+
|
|
3
10
|
## [1.7.3](https://github.com/postalsys/imapflow/compare/v1.7.2...v1.7.3) (2026-08-24)
|
|
4
11
|
|
|
5
12
|
|
package/lib/search-compiler.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
-
const { formatDate, formatFlag, canUseFlag,
|
|
5
|
+
const { formatDate, formatFlag, canUseFlag, toValidDate, isRev2Active } = require('./tools.js');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Sets a boolean flag in the IMAP search attributes.
|
|
@@ -71,15 +71,25 @@ let setOpt = (attributes, term, value) => {
|
|
|
71
71
|
*
|
|
72
72
|
* @param {Array} attributes - Array to append the attribute to
|
|
73
73
|
* @param {string} term - The date search term (e.g., 'BEFORE', 'SINCE')
|
|
74
|
-
* @param {
|
|
74
|
+
* @param {Date|String} value - Date value to format
|
|
75
75
|
*/
|
|
76
76
|
let processDateField = (attributes, term, value) => {
|
|
77
|
-
|
|
77
|
+
// Normalize first. A Date brand check is not enough on its own: an invalid
|
|
78
|
+
// Date is still a Date and toISOString() throws on it. Normalizing here also
|
|
79
|
+
// means a date string behaves exactly like the equivalent Date object.
|
|
80
|
+
value = toValidDate(value);
|
|
81
|
+
if (!value) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (['BEFORE', 'SENTBEFORE'].includes(term.toUpperCase()) && value.toISOString().substring(11) !== '00:00:00.000Z') {
|
|
78
86
|
// Set to next day to include current day as well, othwerise BEFORE+AFTER
|
|
79
87
|
// searches for the same day but different time values do not match anything
|
|
80
88
|
value = new Date(value.getTime() + 24 * 3600 * 1000);
|
|
81
89
|
}
|
|
82
90
|
|
|
91
|
+
// Still reachable after the guard above: the +24h shift can push a near-max
|
|
92
|
+
// Date past the representable range
|
|
83
93
|
let date = formatDate(value);
|
|
84
94
|
if (!date) {
|
|
85
95
|
return;
|
|
@@ -338,18 +348,25 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
338
348
|
case 'BEFORE':
|
|
339
349
|
case 'SINCE':
|
|
340
350
|
{
|
|
351
|
+
// Normalize above the capability check so the WITHIN shortcut
|
|
352
|
+
// and the standard path agree on what counts as a usable date
|
|
353
|
+
let value = toValidDate(params[term]);
|
|
354
|
+
if (!value) {
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
|
|
341
358
|
// Use WITHIN extension for better timezone handling if available
|
|
342
|
-
if (connection.capabilities.has('WITHIN')
|
|
359
|
+
if (connection.capabilities.has('WITHIN')) {
|
|
343
360
|
// Convert to seconds ago from now
|
|
344
361
|
const now = Date.now();
|
|
345
|
-
const withinSeconds = Math.round(Math.max(0, now -
|
|
362
|
+
const withinSeconds = Math.round(Math.max(0, now - value.getTime()) / 1000);
|
|
346
363
|
const withinKeyword = term.toUpperCase() === 'BEFORE' ? 'OLDER' : 'YOUNGER';
|
|
347
364
|
setOpt(attributes, withinKeyword, withinSeconds.toString());
|
|
348
365
|
break;
|
|
349
366
|
}
|
|
350
367
|
|
|
351
368
|
// Fallback to standard date search
|
|
352
|
-
processDateField(attributes, term,
|
|
369
|
+
processDateField(attributes, term, value);
|
|
353
370
|
}
|
|
354
371
|
break;
|
|
355
372
|
|
package/package.json
CHANGED
|
@@ -710,6 +710,57 @@ module.exports['Search Compiler: Date with invalid value ignored'] = test => {
|
|
|
710
710
|
test.done();
|
|
711
711
|
};
|
|
712
712
|
|
|
713
|
+
module.exports['Search Compiler: all date keys ignore an invalid Date object'] = test => {
|
|
714
|
+
// An invalid Date is still a Date, so a plain Date brand check let it through:
|
|
715
|
+
// BEFORE/SENTBEFORE threw RangeError from toISOString(), and with WITHIN
|
|
716
|
+
// advertised BEFORE/SINCE compiled a literal "OLDER NaN"/"YOUNGER NaN" token
|
|
717
|
+
for (let capabilities of [[['IMAP4rev1', true]], [['WITHIN', true]]]) {
|
|
718
|
+
for (let key of ['before', 'since', 'on', 'sentBefore', 'sentOn', 'sentSince']) {
|
|
719
|
+
let connection = createMockConnection({ capabilities });
|
|
720
|
+
let compiled = searchCompiler(connection, { [key]: new Date('not-a-date') });
|
|
721
|
+
|
|
722
|
+
test.deepEqual(compiled, [], `${key} should compile to no attributes`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
test.done();
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
module.exports['Search Compiler: invalid date drops only its own criterion'] = test => {
|
|
730
|
+
let connection = createMockConnection();
|
|
731
|
+
let compiled = searchCompiler(connection, { seen: true, before: new Date('not-a-date') });
|
|
732
|
+
|
|
733
|
+
test.deepEqual(compiled, [{ type: 'ATOM', value: 'SEEN' }]);
|
|
734
|
+
test.done();
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
module.exports['Search Compiler: date string and Date object compile alike for BEFORE'] = test => {
|
|
738
|
+
let asDate = searchCompiler(createMockConnection(), { before: new Date('2023-06-15T12:30:00.000Z') });
|
|
739
|
+
let asString = searchCompiler(createMockConnection(), { before: '2023-06-15T12:30:00.000Z' });
|
|
740
|
+
|
|
741
|
+
// Both input forms are documented, so both must get the +24h shift that makes
|
|
742
|
+
// same-day BEFORE+SINCE ranges match. The string form used to skip it.
|
|
743
|
+
test.deepEqual(asString, asDate);
|
|
744
|
+
test.ok(hasAttr(asString, '16-Jun-2023'));
|
|
745
|
+
test.done();
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
module.exports['Search Compiler: date string and Date object compile alike for WITHIN'] = test => {
|
|
749
|
+
let connection = createMockConnection({ capabilities: [['WITHIN', true]] });
|
|
750
|
+
let recentDate = new Date(Date.now() - 3600 * 1000); // 1 hour ago
|
|
751
|
+
let asDate = searchCompiler(connection, { since: recentDate });
|
|
752
|
+
let asString = searchCompiler(connection, { since: recentDate.toISOString() });
|
|
753
|
+
|
|
754
|
+
// The string form used to skip the WITHIN shortcut and compile SINCE instead
|
|
755
|
+
test.ok(hasAttr(asDate, 'YOUNGER'));
|
|
756
|
+
test.ok(hasAttr(asString, 'YOUNGER'));
|
|
757
|
+
// The keyword atom is followed by its value token
|
|
758
|
+
let seconds = attrs => Number(attrs[1].value);
|
|
759
|
+
// Both are measured against Date.now() at compile time, so allow a small drift
|
|
760
|
+
test.ok(Math.abs(seconds(asString) - seconds(asDate)) <= 1);
|
|
761
|
+
test.done();
|
|
762
|
+
};
|
|
763
|
+
|
|
713
764
|
// ============================================
|
|
714
765
|
// KEYWORD tests
|
|
715
766
|
// ============================================
|