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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.7.3"
2
+ ".": "1.7.4"
3
3
  }
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
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  'use strict';
4
4
 
5
- const { formatDate, formatFlag, canUseFlag, isDate, isRev2Active } = require('./tools.js');
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 {*} value - Date value to format
74
+ * @param {Date|String} value - Date value to format
75
75
  */
76
76
  let processDateField = (attributes, term, value) => {
77
- if (['BEFORE', 'SENTBEFORE'].includes(term.toUpperCase()) && isDate(value) && value.toISOString().substring(11) !== '00:00:00.000Z') {
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') && isDate(params[term])) {
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 - params[term].getTime()) / 1000);
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, params[term]);
369
+ processDateField(attributes, term, value);
353
370
  }
354
371
  break;
355
372
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.7.3",
3
+ "version": "1.7.4",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "lib/imap-flow.js",
6
6
  "types": "lib/imap-flow.d.ts",
@@ -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
  // ============================================