imapflow 1.0.189 → 1.0.191

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.191](https://github.com/postalsys/imapflow/compare/v1.0.190...v1.0.191) (2025-07-10)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **search:** Use the next day when searching BEFORE/SENTBEFORE to include current day in the range as well ([9e6d749](https://github.com/postalsys/imapflow/commit/9e6d749ac781c6240b3113f2ec8d329ad831d740))
9
+
10
+ ## [1.0.190](https://github.com/postalsys/imapflow/compare/v1.0.189...v1.0.190) (2025-07-10)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **search:** Prefer WITHIN YOUNGER/OLDER instead of BEFORE/SINCE for searches if possible ([7492ac9](https://github.com/postalsys/imapflow/commit/7492ac9bd5598beadee7a72e3e3c63e25e9bc1f5))
16
+
3
17
  ## [1.0.189](https://github.com/postalsys/imapflow/compare/v1.0.188...v1.0.189) (2025-06-30)
4
18
 
5
19
 
@@ -1,12 +1,28 @@
1
- 'use strict';
1
+ /* eslint no-control-regex:0 */
2
2
 
3
- const { formatDate, formatFlag, canUseFlag } = require('./tools.js');
3
+ 'use strict';
4
4
 
5
+ const { formatDate, formatFlag, canUseFlag, isDate } = require('./tools.js');
6
+
7
+ /**
8
+ * Sets a boolean flag in the IMAP search attributes.
9
+ * Automatically handles UN- prefixing for falsy values.
10
+ *
11
+ * @param {Array} attributes - Array to append the attribute to
12
+ * @param {string} term - The flag name (e.g., 'SEEN', 'DELETED')
13
+ * @param {boolean} value - Whether to set or unset the flag
14
+ * @example
15
+ * setBoolOpt(attributes, 'SEEN', false) // Adds 'UNSEEN'
16
+ * setBoolOpt(attributes, 'UNSEEN', false) // Adds 'SEEN' (removes UN prefix)
17
+ */
5
18
  let setBoolOpt = (attributes, term, value) => {
6
19
  if (!value) {
20
+ // For falsy values, toggle the UN- prefix
7
21
  if (/^un/i.test(term)) {
22
+ // Remove existing UN prefix
8
23
  term = term.slice(2);
9
24
  } else {
25
+ // Add UN prefix
10
26
  term = 'UN' + term;
11
27
  }
12
28
  }
@@ -14,15 +30,26 @@ let setBoolOpt = (attributes, term, value) => {
14
30
  attributes.push({ type: 'ATOM', value: term.toUpperCase() });
15
31
  };
16
32
 
33
+ /**
34
+ * Adds a search option with its value(s) to the attributes array.
35
+ * Handles NOT operations and array values.
36
+ *
37
+ * @param {Array} attributes - Array to append the attribute to
38
+ * @param {string} term - The search term (e.g., 'FROM', 'SUBJECT')
39
+ * @param {*} value - The value for the search term (string, array, or falsy for NOT)
40
+ * @param {string} [type='ATOM'] - The attribute type
41
+ */
17
42
  let setOpt = (attributes, term, value, type) => {
18
43
  type = type || 'ATOM';
19
44
 
45
+ // Handle NOT operations for false or null values
20
46
  if (value === false || value === null) {
21
47
  attributes.push({ type, value: 'NOT' });
22
48
  }
23
49
 
24
50
  attributes.push({ type, value: term.toUpperCase() });
25
51
 
52
+ // Handle array values (e.g., multiple UIDs)
26
53
  if (Array.isArray(value)) {
27
54
  value.forEach(entry => attributes.push({ type, value: (entry || '').toString() }));
28
55
  } else {
@@ -30,7 +57,21 @@ let setOpt = (attributes, term, value, type) => {
30
57
  }
31
58
  };
32
59
 
60
+ /**
61
+ * Processes date fields for IMAP search.
62
+ * Converts JavaScript dates to IMAP date format.
63
+ *
64
+ * @param {Array} attributes - Array to append the attribute to
65
+ * @param {string} term - The date search term (e.g., 'BEFORE', 'SINCE')
66
+ * @param {*} value - Date value to format
67
+ */
33
68
  let processDateField = (attributes, term, value) => {
69
+ if (['BEFORE', 'SENTBEFORE'].includes(term.toUpperCase()) && isDate(value) && value.toISOString().substring(11) !== '00:00:00.000Z') {
70
+ // Set to next day to include current day as well, othwerise BEFORE+AFTER
71
+ // searches for the same day but different time values do not match anything
72
+ value = new Date(value.getTime() + 24 * 3600 * 1000);
73
+ }
74
+
34
75
  let date = formatDate(value);
35
76
  if (!date) {
36
77
  return;
@@ -39,35 +80,85 @@ let processDateField = (attributes, term, value) => {
39
80
  setOpt(attributes, term, date);
40
81
  };
41
82
 
83
+ // Pre-compiled regex for better performance
84
+ const UNICODE_PATTERN = /[^\x00-\x7F]/;
85
+
86
+ /**
87
+ * Checks if a string contains Unicode characters.
88
+ * Used to determine if CHARSET UTF-8 needs to be specified.
89
+ *
90
+ * @param {*} str - String to check
91
+ * @returns {boolean} True if string contains non-ASCII characters
92
+ */
42
93
  let isUnicodeString = str => {
43
94
  if (!str || typeof str !== 'string') {
44
95
  return false;
45
96
  }
46
97
 
47
- return Buffer.byteLength(str) !== str.length;
98
+ // Regex test is ~3-5x faster than Buffer.byteLength
99
+ // Matches any character outside ASCII range (0x00-0x7F)
100
+ return UNICODE_PATTERN.test(str);
48
101
  };
49
102
 
103
+ /**
104
+ * Compiles a JavaScript object query into IMAP search command attributes.
105
+ * Supports standard IMAP search criteria and extensions like OBJECTID and Gmail extensions.
106
+ *
107
+ * @param {Object} connection - IMAP connection object
108
+ * @param {Object} connection.capabilities - Set of server capabilities
109
+ * @param {Object} connection.enabled - Set of enabled extensions
110
+ * @param {Object} connection.mailbox - Current mailbox information
111
+ * @param {Set} connection.mailbox.flags - Available flags in the mailbox
112
+ * @param {Object} query - Search query object
113
+ * @returns {Array} Array of IMAP search attributes
114
+ * @throws {Error} When required server extensions are not available
115
+ *
116
+ * @example
117
+ * // Simple search for unseen messages from a sender
118
+ * searchCompiler(connection, {
119
+ * unseen: true,
120
+ * from: 'sender@example.com'
121
+ * });
122
+ *
123
+ * @example
124
+ * // Complex OR search with date range
125
+ * searchCompiler(connection, {
126
+ * or: [
127
+ * { from: 'alice@example.com' },
128
+ * { from: 'bob@example.com' }
129
+ * ],
130
+ * since: new Date('2024-01-01')
131
+ * });
132
+ */
50
133
  module.exports.searchCompiler = (connection, query) => {
51
134
  const attributes = [];
52
135
 
136
+ // Track if we need to specify UTF-8 charset
53
137
  let hasUnicode = false;
54
138
  const mailbox = connection.mailbox;
55
139
 
140
+ /**
141
+ * Recursively walks through the query object and builds IMAP attributes.
142
+ * @param {Object} params - Query parameters to process
143
+ */
56
144
  const walk = params => {
57
145
  Object.keys(params || {}).forEach(term => {
58
146
  switch (term.toUpperCase()) {
59
- case 'SEQ': // custom key for sequence range
147
+ // Custom sequence range support (non-standard)
148
+ case 'SEQ':
60
149
  {
61
150
  let value = params[term];
62
151
  if (typeof value === 'number') {
63
152
  value = value.toString();
64
153
  }
154
+ // Only accept valid sequence strings (no whitespace)
65
155
  if (typeof value === 'string' && /^\S+$/.test(value)) {
66
156
  attributes.push({ type: 'SEQUENCE', value });
67
157
  }
68
158
  }
69
159
  break;
70
160
 
161
+ // Boolean flags that support UN- prefixing
71
162
  case 'ANSWERED':
72
163
  case 'DELETED':
73
164
  case 'DRAFT':
@@ -82,6 +173,7 @@ module.exports.searchCompiler = (connection, query) => {
82
173
  setBoolOpt(attributes, term, !!params[term]);
83
174
  break;
84
175
 
176
+ // Simple boolean flags without UN- support
85
177
  case 'ALL':
86
178
  case 'NEW':
87
179
  case 'OLD':
@@ -91,6 +183,7 @@ module.exports.searchCompiler = (connection, query) => {
91
183
  }
92
184
  break;
93
185
 
186
+ // Numeric comparisons
94
187
  case 'LARGER':
95
188
  case 'SMALLER':
96
189
  case 'MODSEQ':
@@ -99,6 +192,7 @@ module.exports.searchCompiler = (connection, query) => {
99
192
  }
100
193
  break;
101
194
 
195
+ // Text search fields - check for Unicode
102
196
  case 'BCC':
103
197
  case 'BODY':
104
198
  case 'CC':
@@ -114,28 +208,34 @@ module.exports.searchCompiler = (connection, query) => {
114
208
  }
115
209
  break;
116
210
 
211
+ // UID sequences
117
212
  case 'UID':
118
213
  if (params[term]) {
119
214
  setOpt(attributes, term, params[term], 'SEQUENCE');
120
215
  }
121
216
  break;
122
217
 
218
+ // Email ID support (OBJECTID or Gmail extension)
123
219
  case 'EMAILID':
124
220
  if (connection.capabilities.has('OBJECTID')) {
125
221
  setOpt(attributes, 'EMAILID', params[term]);
126
222
  } else if (connection.capabilities.has('X-GM-EXT-1')) {
223
+ // Fallback to Gmail message ID
127
224
  setOpt(attributes, 'X-GM-MSGID', params[term]);
128
225
  }
129
226
  break;
130
227
 
228
+ // Thread ID support (OBJECTID or Gmail extension)
131
229
  case 'THREADID':
132
230
  if (connection.capabilities.has('OBJECTID')) {
133
231
  setOpt(attributes, 'THREADID', params[term]);
134
232
  } else if (connection.capabilities.has('X-GM-EXT-1')) {
233
+ // Fallback to Gmail thread ID
135
234
  setOpt(attributes, 'X-GM-THRID', params[term]);
136
235
  }
137
236
  break;
138
237
 
238
+ // Gmail raw search
139
239
  case 'GMRAW':
140
240
  case 'GMAILRAW': // alias for GMRAW
141
241
  if (connection.capabilities.has('X-GM-EXT-1')) {
@@ -150,33 +250,65 @@ module.exports.searchCompiler = (connection, query) => {
150
250
  }
151
251
  break;
152
252
 
253
+ // Date searches with WITHIN extension support
153
254
  case 'BEFORE':
154
- case 'ON':
155
255
  case 'SINCE':
256
+ {
257
+ // Use WITHIN extension for better timezone handling if available
258
+ if (connection.capabilities.has('WITHIN') && isDate(params[term])) {
259
+ // Convert to seconds ago from now
260
+ const now = Date.now();
261
+ const withinSeconds = Math.round(Math.max(0, now - params[term].getTime()) / 1000);
262
+ let withinKeyword;
263
+ switch (term.toUpperCase()) {
264
+ case 'BEFORE':
265
+ withinKeyword = 'OLDER';
266
+ break;
267
+ case 'SINCE':
268
+ withinKeyword = 'YOUNGER';
269
+ break;
270
+ }
271
+ setOpt(attributes, withinKeyword, withinSeconds.toString());
272
+ break;
273
+ }
274
+
275
+ // Fallback to standard date search
276
+ processDateField(attributes, term, params[term]);
277
+ }
278
+ break;
279
+
280
+ // Standard date searches
281
+ case 'ON':
156
282
  case 'SENTBEFORE':
157
283
  case 'SENTON':
158
284
  case 'SENTSINCE':
159
285
  processDateField(attributes, term, params[term]);
160
286
  break;
161
287
 
288
+ // Keyword/flag searches
162
289
  case 'KEYWORD':
163
290
  case 'UNKEYWORD':
164
291
  {
165
292
  let flag = formatFlag(params[term]);
293
+ // Only add if flag is supported or already exists in mailbox
166
294
  if (canUseFlag(mailbox, flag) || mailbox.flags.has(flag)) {
167
295
  setOpt(attributes, term, flag);
168
296
  }
169
297
  }
170
298
  break;
171
299
 
300
+ // Header field searches
172
301
  case 'HEADER':
173
302
  if (params[term] && typeof params[term] === 'object') {
174
303
  Object.keys(params[term]).forEach(header => {
175
304
  let value = params[term][header];
305
+
306
+ // Allow boolean true to search for header existence
176
307
  if (value === true) {
177
308
  value = '';
178
309
  }
179
310
 
311
+ // Skip non-string values (after true->'' conversion)
180
312
  if (typeof value !== 'string') {
181
313
  return;
182
314
  }
@@ -190,25 +322,29 @@ module.exports.searchCompiler = (connection, query) => {
190
322
  }
191
323
  break;
192
324
 
325
+ // NOT operator
193
326
  case 'NOT':
194
- {
327
+ {
195
328
  if (!params[term]) {
196
329
  break;
197
330
  }
198
331
 
199
332
  if (typeof params[term] === 'object') {
200
333
  attributes.push({ type: 'ATOM', value: 'NOT' });
334
+ // Recursively process NOT conditions
201
335
  walk(params[term]);
202
336
  }
203
337
  }
204
338
  break;
205
-
339
+
340
+ // OR operator - complex logic for building OR trees
206
341
  case 'OR':
207
342
  {
208
343
  if (!params[term] || !Array.isArray(params[term]) || !params[term].length) {
209
344
  break;
210
345
  }
211
346
 
347
+ // Single element - just process it directly
212
348
  if (params[term].length === 1) {
213
349
  if (typeof params[term][0] === 'object' && params[term][0]) {
214
350
  walk(params[term][0]);
@@ -216,12 +352,18 @@ module.exports.searchCompiler = (connection, query) => {
216
352
  break;
217
353
  }
218
354
 
219
- // OR values has to be grouped by 2
220
- // OR conditional1 conditional2
355
+ /**
356
+ * Generates a binary tree structure for OR operations.
357
+ * IMAP OR takes exactly 2 operands, so we need to nest them.
358
+ *
359
+ * @param {Array} list - List of conditions to OR together
360
+ * @returns {Array} Binary tree structure
361
+ */
221
362
  let genOrTree = list => {
222
363
  let group = false;
223
364
  let groups = [];
224
365
 
366
+ // Group items in pairs
225
367
  list.forEach((entry, i) => {
226
368
  if (i % 2 === 0) {
227
369
  group = [entry];
@@ -232,6 +374,7 @@ module.exports.searchCompiler = (connection, query) => {
232
374
  }
233
375
  });
234
376
 
377
+ // Handle odd number of items
235
378
  if (group && group.length) {
236
379
  while (group.length === 1 && Array.isArray(group[0])) {
237
380
  group = group[0];
@@ -240,10 +383,12 @@ module.exports.searchCompiler = (connection, query) => {
240
383
  groups.push(group);
241
384
  }
242
385
 
386
+ // Recursively group until we have a binary tree
243
387
  while (groups.length > 2) {
244
388
  groups = genOrTree(groups);
245
389
  }
246
390
 
391
+ // Flatten single-element arrays
247
392
  while (groups.length === 1 && Array.isArray(groups[0])) {
248
393
  groups = groups[0];
249
394
  }
@@ -251,8 +396,13 @@ module.exports.searchCompiler = (connection, query) => {
251
396
  return groups;
252
397
  };
253
398
 
399
+ /**
400
+ * Walks the OR tree and generates IMAP commands.
401
+ * @param {Array|Object} entry - Tree node to process
402
+ */
254
403
  let walkOrTree = entry => {
255
404
  if (Array.isArray(entry)) {
405
+ // Only add OR for multiple items
256
406
  if (entry.length > 1) {
257
407
  attributes.push({ type: 'ATOM', value: 'OR' });
258
408
  }
@@ -263,6 +413,7 @@ module.exports.searchCompiler = (connection, query) => {
263
413
  walk(entry);
264
414
  }
265
415
  };
416
+
266
417
  walkOrTree(genOrTree(params[term]));
267
418
  }
268
419
  break;
@@ -270,10 +421,12 @@ module.exports.searchCompiler = (connection, query) => {
270
421
  });
271
422
  };
272
423
 
424
+ // Process the query
273
425
  walk(query);
274
426
 
427
+ // If we encountered Unicode strings and UTF-8 is not already accepted,
428
+ // prepend CHARSET UTF-8 to the search command
275
429
  if (hasUnicode && !connection.enabled.has('UTF8=ACCEPT')) {
276
- // Prepend search query with `CHARSET UTF-8`
277
430
  attributes.unshift({ type: 'ATOM', value: 'UTF-8' });
278
431
  attributes.unshift({ type: 'ATOM', value: 'CHARSET' });
279
432
  }
package/lib/tools.js CHANGED
@@ -754,6 +754,10 @@ const tools = {
754
754
  return walk(entry);
755
755
  },
756
756
 
757
+ isDate(obj) {
758
+ return Object.prototype.toString.call(obj) === '[object Date]';
759
+ },
760
+
757
761
  formatDate(value) {
758
762
  if (typeof value === 'string') {
759
763
  value = new Date(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.189",
3
+ "version": "1.0.191",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "lib/imap-flow.js",
6
6
  "types": "lib/imap-flow.d.ts",
@@ -28,11 +28,11 @@
28
28
  },
29
29
  "homepage": "https://imapflow.com/",
30
30
  "devDependencies": {
31
- "@babel/eslint-parser": "7.27.5",
31
+ "@babel/eslint-parser": "7.28.0",
32
32
  "@babel/eslint-plugin": "7.27.1",
33
33
  "@babel/plugin-syntax-class-properties": "7.12.13",
34
- "@babel/preset-env": "7.27.2",
35
- "@types/node": "24.0.7",
34
+ "@babel/preset-env": "7.28.0",
35
+ "@types/node": "24.0.12",
36
36
  "eslint": "8.57.0",
37
37
  "eslint-config-nodemailer": "1.2.0",
38
38
  "eslint-config-prettier": "9.1.0",
@@ -52,7 +52,7 @@
52
52
  "libmime": "5.3.7",
53
53
  "libqp": "2.1.1",
54
54
  "mailsplit": "5.4.5",
55
- "nodemailer": "7.0.4",
55
+ "nodemailer": "7.0.5",
56
56
  "pino": "9.7.0",
57
57
  "socks": "2.8.5"
58
58
  }