imapflow 2.2.2 → 2.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.3](https://github.com/postalsys/imapflow/compare/v2.2.2...v2.2.3) (2026-10-03)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * reject NUL in a non-ASCII search value instead of sending it as a literal ([8c35084](https://github.com/postalsys/imapflow/commit/8c35084f12376a1195989bf99621c2587f548aca))
9
+ * send non-ASCII search values as literals ([b58d93e](https://github.com/postalsys/imapflow/commit/b58d93e49dd37ef008418c2670afaf53b3d78034)), closes [#417](https://github.com/postalsys/imapflow/issues/417)
10
+
3
11
  ## [2.2.2](https://github.com/postalsys/imapflow/compare/v2.2.1...v2.2.2) (2026-10-03)
4
12
 
5
13
 
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.2.2";
2
+ export declare const version = "2.2.3";
3
3
  export declare const homepage = "https://imapflow.com/";
@@ -3,5 +3,5 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.homepage = exports.version = exports.name = void 0;
5
5
  exports.name = "imapflow";
6
- exports.version = "2.2.2";
6
+ exports.version = "2.2.3";
7
7
  exports.homepage = "https://imapflow.com/";
@@ -3,6 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.searchCompiler = void 0;
5
5
  const tools_js_1 = require("./tools.js");
6
+ // Matches any character outside the ASCII range
7
+ const UNICODE_PATTERN = /[^\x00-\x7F]/;
6
8
  /**
7
9
  * Sets a boolean flag in the IMAP search attributes.
8
10
  * Automatically handles UN- prefixing for falsy values.
@@ -38,6 +40,17 @@ let setBoolOpt = (attributes, term, value) => {
38
40
  * @returns The joined sequence set string
39
41
  */
40
42
  let toSequenceValue = (value) => [].concat(value).join(',');
43
+ /**
44
+ * Builds the token for a search value. A quoted string may only carry 7-bit
45
+ * characters (RFC 3501 section 9), so a non-ASCII value is sent as a literal;
46
+ * strict servers reply BAD to UTF-8 inside a quoted string. A literal can not
47
+ * carry NUL either (CHAR8 is %x01-ff), so such a value stays an ATOM and the
48
+ * compiler rejects it.
49
+ *
50
+ * @param value - The search value
51
+ * @returns An ATOM token (quoted by the compiler when needed), or a LITERAL token
52
+ */
53
+ let toSearchValue = (value) => UNICODE_PATTERN.test(value) && !value.includes('\0') ? { type: 'LITERAL', value: Buffer.from(value) } : { type: 'ATOM', value };
41
54
  /**
42
55
  * Adds a search option with its value(s) to the attributes array.
43
56
  * Handles NOT operations and array values.
@@ -54,10 +67,10 @@ let setOpt = (attributes, term, value) => {
54
67
  attributes.push({ type: 'ATOM', value: term.toUpperCase() });
55
68
  // Handle array values (e.g. HEADER name/value pairs)
56
69
  if (Array.isArray(value)) {
57
- value.forEach(entry => attributes.push({ type: 'ATOM', value: (entry || '').toString() }));
70
+ value.forEach(entry => attributes.push(toSearchValue((entry || '').toString())));
58
71
  }
59
72
  else {
60
- attributes.push({ type: 'ATOM', value: value.toString() });
73
+ attributes.push(toSearchValue(value.toString()));
61
74
  }
62
75
  };
63
76
  /**
@@ -89,8 +102,6 @@ let processDateField = (attributes, term, value) => {
89
102
  }
90
103
  setOpt(attributes, term, formatted);
91
104
  };
92
- // Pre-compiled regex for better performance
93
- const UNICODE_PATTERN = /[^\x00-\x7F]/;
94
105
  /**
95
106
  * Throws a coded search compilation error.
96
107
  *
@@ -103,20 +114,13 @@ let fail = (code, message) => {
103
114
  throw error;
104
115
  };
105
116
  /**
106
- * Checks if a string contains Unicode characters.
107
- * Used to determine if CHARSET UTF-8 needs to be specified.
117
+ * Checks whether any search value was compiled into a literal, which only
118
+ * happens for non-ASCII values and means CHARSET UTF-8 needs to be specified.
108
119
  *
109
- * @param str - String to check
110
- * @returns True if string contains non-ASCII characters
120
+ * @param attributes - Compiled search attributes
121
+ * @returns True if a LITERAL token is present
111
122
  */
112
- let isUnicodeString = (str) => {
113
- if (!str || typeof str !== 'string') {
114
- return false;
115
- }
116
- // Regex test is ~3-5x faster than Buffer.byteLength
117
- // Matches any character outside ASCII range (0x00-0x7F)
118
- return UNICODE_PATTERN.test(str);
119
- };
123
+ let hasLiteral = (attributes) => attributes.some(attr => (Array.isArray(attr) ? hasLiteral(attr) : attr.type === 'LITERAL'));
120
124
  /**
121
125
  * Compiles a JavaScript object query into IMAP search command attributes.
122
126
  * Supports standard IMAP search criteria and extensions like OBJECTID and Gmail extensions.
@@ -145,8 +149,6 @@ let isUnicodeString = (str) => {
145
149
  */
146
150
  const searchCompiler = (connection, query) => {
147
151
  const attributes = [];
148
- // Track if we need to specify UTF-8 charset
149
- let hasUnicode = false;
150
152
  /**
151
153
  * Recursively walks through the query object and builds IMAP attributes.
152
154
  * @param params - Query parameters to process
@@ -236,9 +238,6 @@ const searchCompiler = (connection, query) => {
236
238
  case 'SUBJECT':
237
239
  case 'TEXT':
238
240
  case 'TO':
239
- if (isUnicodeString(params[term])) {
240
- hasUnicode = true;
241
- }
242
241
  if (params[term]) {
243
242
  setOpt(attributes, term, params[term]);
244
243
  }
@@ -286,9 +285,6 @@ const searchCompiler = (connection, query) => {
286
285
  case 'GMRAW':
287
286
  case 'GMAILRAW': // alias for GMRAW
288
287
  if (connection.capabilities.has('X-GM-EXT-1')) {
289
- if (isUnicodeString(params[term])) {
290
- hasUnicode = true;
291
- }
292
288
  setOpt(attributes, 'X-GM-RAW', params[term]);
293
289
  }
294
290
  else {
@@ -329,9 +325,6 @@ const searchCompiler = (connection, query) => {
329
325
  fail('MissingServerExtension', 'Server does not support X-GM-EXT-1 extension required for label search');
330
326
  }
331
327
  let rawQuery = rawParts.join(' ');
332
- if (isUnicodeString(rawQuery)) {
333
- hasUnicode = true;
334
- }
335
328
  setOpt(attributes, 'X-GM-RAW', rawQuery);
336
329
  break;
337
330
  }
@@ -391,9 +384,6 @@ const searchCompiler = (connection, query) => {
391
384
  if (typeof value !== 'string') {
392
385
  return;
393
386
  }
394
- if (isUnicodeString(value)) {
395
- hasUnicode = true;
396
- }
397
387
  setOpt(attributes, term, [header.toUpperCase().trim(), value]);
398
388
  });
399
389
  }
@@ -473,7 +463,7 @@ const searchCompiler = (connection, query) => {
473
463
  walk(query);
474
464
  // If we encountered Unicode strings and UTF-8 is not already accepted,
475
465
  // prepend CHARSET UTF-8 to the search command
476
- if (hasUnicode && !connection.enabled.has('UTF8=ACCEPT')) {
466
+ if (!connection.enabled.has('UTF8=ACCEPT') && hasLiteral(attributes)) {
477
467
  attributes.unshift({ type: 'ATOM', value: 'UTF-8' });
478
468
  attributes.unshift({ type: 'ATOM', value: 'CHARSET' });
479
469
  }
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.2.2";
2
+ export declare const version = "2.2.3";
3
3
  export declare const homepage = "https://imapflow.com/";
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/build.js from package.json. Do not edit by hand.
2
2
  export const name = "imapflow";
3
- export const version = "2.2.2";
3
+ export const version = "2.2.3";
4
4
  export const homepage = "https://imapflow.com/";
@@ -1,5 +1,7 @@
1
1
  /* eslint no-control-regex:0 */
2
2
  import { formatDate, formatFlag, toValidDate, isRev2Active } from './tools.js';
3
+ // Matches any character outside the ASCII range
4
+ const UNICODE_PATTERN = /[^\x00-\x7F]/;
3
5
  /**
4
6
  * Sets a boolean flag in the IMAP search attributes.
5
7
  * Automatically handles UN- prefixing for falsy values.
@@ -35,6 +37,17 @@ let setBoolOpt = (attributes, term, value) => {
35
37
  * @returns The joined sequence set string
36
38
  */
37
39
  let toSequenceValue = (value) => [].concat(value).join(',');
40
+ /**
41
+ * Builds the token for a search value. A quoted string may only carry 7-bit
42
+ * characters (RFC 3501 section 9), so a non-ASCII value is sent as a literal;
43
+ * strict servers reply BAD to UTF-8 inside a quoted string. A literal can not
44
+ * carry NUL either (CHAR8 is %x01-ff), so such a value stays an ATOM and the
45
+ * compiler rejects it.
46
+ *
47
+ * @param value - The search value
48
+ * @returns An ATOM token (quoted by the compiler when needed), or a LITERAL token
49
+ */
50
+ let toSearchValue = (value) => UNICODE_PATTERN.test(value) && !value.includes('\0') ? { type: 'LITERAL', value: Buffer.from(value) } : { type: 'ATOM', value };
38
51
  /**
39
52
  * Adds a search option with its value(s) to the attributes array.
40
53
  * Handles NOT operations and array values.
@@ -51,10 +64,10 @@ let setOpt = (attributes, term, value) => {
51
64
  attributes.push({ type: 'ATOM', value: term.toUpperCase() });
52
65
  // Handle array values (e.g. HEADER name/value pairs)
53
66
  if (Array.isArray(value)) {
54
- value.forEach(entry => attributes.push({ type: 'ATOM', value: (entry || '').toString() }));
67
+ value.forEach(entry => attributes.push(toSearchValue((entry || '').toString())));
55
68
  }
56
69
  else {
57
- attributes.push({ type: 'ATOM', value: value.toString() });
70
+ attributes.push(toSearchValue(value.toString()));
58
71
  }
59
72
  };
60
73
  /**
@@ -86,8 +99,6 @@ let processDateField = (attributes, term, value) => {
86
99
  }
87
100
  setOpt(attributes, term, formatted);
88
101
  };
89
- // Pre-compiled regex for better performance
90
- const UNICODE_PATTERN = /[^\x00-\x7F]/;
91
102
  /**
92
103
  * Throws a coded search compilation error.
93
104
  *
@@ -100,20 +111,13 @@ let fail = (code, message) => {
100
111
  throw error;
101
112
  };
102
113
  /**
103
- * Checks if a string contains Unicode characters.
104
- * Used to determine if CHARSET UTF-8 needs to be specified.
114
+ * Checks whether any search value was compiled into a literal, which only
115
+ * happens for non-ASCII values and means CHARSET UTF-8 needs to be specified.
105
116
  *
106
- * @param str - String to check
107
- * @returns True if string contains non-ASCII characters
117
+ * @param attributes - Compiled search attributes
118
+ * @returns True if a LITERAL token is present
108
119
  */
109
- let isUnicodeString = (str) => {
110
- if (!str || typeof str !== 'string') {
111
- return false;
112
- }
113
- // Regex test is ~3-5x faster than Buffer.byteLength
114
- // Matches any character outside ASCII range (0x00-0x7F)
115
- return UNICODE_PATTERN.test(str);
116
- };
120
+ let hasLiteral = (attributes) => attributes.some(attr => (Array.isArray(attr) ? hasLiteral(attr) : attr.type === 'LITERAL'));
117
121
  /**
118
122
  * Compiles a JavaScript object query into IMAP search command attributes.
119
123
  * Supports standard IMAP search criteria and extensions like OBJECTID and Gmail extensions.
@@ -142,8 +146,6 @@ let isUnicodeString = (str) => {
142
146
  */
143
147
  export const searchCompiler = (connection, query) => {
144
148
  const attributes = [];
145
- // Track if we need to specify UTF-8 charset
146
- let hasUnicode = false;
147
149
  /**
148
150
  * Recursively walks through the query object and builds IMAP attributes.
149
151
  * @param params - Query parameters to process
@@ -233,9 +235,6 @@ export const searchCompiler = (connection, query) => {
233
235
  case 'SUBJECT':
234
236
  case 'TEXT':
235
237
  case 'TO':
236
- if (isUnicodeString(params[term])) {
237
- hasUnicode = true;
238
- }
239
238
  if (params[term]) {
240
239
  setOpt(attributes, term, params[term]);
241
240
  }
@@ -283,9 +282,6 @@ export const searchCompiler = (connection, query) => {
283
282
  case 'GMRAW':
284
283
  case 'GMAILRAW': // alias for GMRAW
285
284
  if (connection.capabilities.has('X-GM-EXT-1')) {
286
- if (isUnicodeString(params[term])) {
287
- hasUnicode = true;
288
- }
289
285
  setOpt(attributes, 'X-GM-RAW', params[term]);
290
286
  }
291
287
  else {
@@ -326,9 +322,6 @@ export const searchCompiler = (connection, query) => {
326
322
  fail('MissingServerExtension', 'Server does not support X-GM-EXT-1 extension required for label search');
327
323
  }
328
324
  let rawQuery = rawParts.join(' ');
329
- if (isUnicodeString(rawQuery)) {
330
- hasUnicode = true;
331
- }
332
325
  setOpt(attributes, 'X-GM-RAW', rawQuery);
333
326
  break;
334
327
  }
@@ -388,9 +381,6 @@ export const searchCompiler = (connection, query) => {
388
381
  if (typeof value !== 'string') {
389
382
  return;
390
383
  }
391
- if (isUnicodeString(value)) {
392
- hasUnicode = true;
393
- }
394
384
  setOpt(attributes, term, [header.toUpperCase().trim(), value]);
395
385
  });
396
386
  }
@@ -470,7 +460,7 @@ export const searchCompiler = (connection, query) => {
470
460
  walk(query);
471
461
  // If we encountered Unicode strings and UTF-8 is not already accepted,
472
462
  // prepend CHARSET UTF-8 to the search command
473
- if (hasUnicode && !connection.enabled.has('UTF8=ACCEPT')) {
463
+ if (!connection.enabled.has('UTF8=ACCEPT') && hasLiteral(attributes)) {
474
464
  attributes.unshift({ type: 'ATOM', value: 'UTF-8' });
475
465
  attributes.unshift({ type: 'ATOM', value: 'CHARSET' });
476
466
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
4
4
  "description": "IMAP Client for Node",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/imap-flow.js",