imapflow 1.0.188 → 1.0.190
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 +15 -0
- package/lib/handler/imap-parser.js +8 -3
- package/lib/imap-flow.d.ts +786 -0
- package/lib/imap-flow.js +220 -89
- package/lib/search-compiler.js +157 -10
- package/lib/tools.js +4 -0
- package/package.json +11 -11
- package/lib/types.d.ts +0 -1123
- package/types.js +0 -56
package/lib/search-compiler.js
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint no-control-regex:0 */
|
|
2
2
|
|
|
3
|
-
|
|
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,6 +57,14 @@ 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) => {
|
|
34
69
|
let date = formatDate(value);
|
|
35
70
|
if (!date) {
|
|
@@ -39,35 +74,85 @@ let processDateField = (attributes, term, value) => {
|
|
|
39
74
|
setOpt(attributes, term, date);
|
|
40
75
|
};
|
|
41
76
|
|
|
77
|
+
// Pre-compiled regex for better performance
|
|
78
|
+
const UNICODE_PATTERN = /[^\x00-\x7F]/;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Checks if a string contains Unicode characters.
|
|
82
|
+
* Used to determine if CHARSET UTF-8 needs to be specified.
|
|
83
|
+
*
|
|
84
|
+
* @param {*} str - String to check
|
|
85
|
+
* @returns {boolean} True if string contains non-ASCII characters
|
|
86
|
+
*/
|
|
42
87
|
let isUnicodeString = str => {
|
|
43
88
|
if (!str || typeof str !== 'string') {
|
|
44
89
|
return false;
|
|
45
90
|
}
|
|
46
91
|
|
|
47
|
-
|
|
92
|
+
// Regex test is ~3-5x faster than Buffer.byteLength
|
|
93
|
+
// Matches any character outside ASCII range (0x00-0x7F)
|
|
94
|
+
return UNICODE_PATTERN.test(str);
|
|
48
95
|
};
|
|
49
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Compiles a JavaScript object query into IMAP search command attributes.
|
|
99
|
+
* Supports standard IMAP search criteria and extensions like OBJECTID and Gmail extensions.
|
|
100
|
+
*
|
|
101
|
+
* @param {Object} connection - IMAP connection object
|
|
102
|
+
* @param {Object} connection.capabilities - Set of server capabilities
|
|
103
|
+
* @param {Object} connection.enabled - Set of enabled extensions
|
|
104
|
+
* @param {Object} connection.mailbox - Current mailbox information
|
|
105
|
+
* @param {Set} connection.mailbox.flags - Available flags in the mailbox
|
|
106
|
+
* @param {Object} query - Search query object
|
|
107
|
+
* @returns {Array} Array of IMAP search attributes
|
|
108
|
+
* @throws {Error} When required server extensions are not available
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* // Simple search for unseen messages from a sender
|
|
112
|
+
* searchCompiler(connection, {
|
|
113
|
+
* unseen: true,
|
|
114
|
+
* from: 'sender@example.com'
|
|
115
|
+
* });
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* // Complex OR search with date range
|
|
119
|
+
* searchCompiler(connection, {
|
|
120
|
+
* or: [
|
|
121
|
+
* { from: 'alice@example.com' },
|
|
122
|
+
* { from: 'bob@example.com' }
|
|
123
|
+
* ],
|
|
124
|
+
* since: new Date('2024-01-01')
|
|
125
|
+
* });
|
|
126
|
+
*/
|
|
50
127
|
module.exports.searchCompiler = (connection, query) => {
|
|
51
128
|
const attributes = [];
|
|
52
129
|
|
|
130
|
+
// Track if we need to specify UTF-8 charset
|
|
53
131
|
let hasUnicode = false;
|
|
54
132
|
const mailbox = connection.mailbox;
|
|
55
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Recursively walks through the query object and builds IMAP attributes.
|
|
136
|
+
* @param {Object} params - Query parameters to process
|
|
137
|
+
*/
|
|
56
138
|
const walk = params => {
|
|
57
139
|
Object.keys(params || {}).forEach(term => {
|
|
58
140
|
switch (term.toUpperCase()) {
|
|
59
|
-
|
|
141
|
+
// Custom sequence range support (non-standard)
|
|
142
|
+
case 'SEQ':
|
|
60
143
|
{
|
|
61
144
|
let value = params[term];
|
|
62
145
|
if (typeof value === 'number') {
|
|
63
146
|
value = value.toString();
|
|
64
147
|
}
|
|
148
|
+
// Only accept valid sequence strings (no whitespace)
|
|
65
149
|
if (typeof value === 'string' && /^\S+$/.test(value)) {
|
|
66
150
|
attributes.push({ type: 'SEQUENCE', value });
|
|
67
151
|
}
|
|
68
152
|
}
|
|
69
153
|
break;
|
|
70
154
|
|
|
155
|
+
// Boolean flags that support UN- prefixing
|
|
71
156
|
case 'ANSWERED':
|
|
72
157
|
case 'DELETED':
|
|
73
158
|
case 'DRAFT':
|
|
@@ -82,6 +167,7 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
82
167
|
setBoolOpt(attributes, term, !!params[term]);
|
|
83
168
|
break;
|
|
84
169
|
|
|
170
|
+
// Simple boolean flags without UN- support
|
|
85
171
|
case 'ALL':
|
|
86
172
|
case 'NEW':
|
|
87
173
|
case 'OLD':
|
|
@@ -91,6 +177,7 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
91
177
|
}
|
|
92
178
|
break;
|
|
93
179
|
|
|
180
|
+
// Numeric comparisons
|
|
94
181
|
case 'LARGER':
|
|
95
182
|
case 'SMALLER':
|
|
96
183
|
case 'MODSEQ':
|
|
@@ -99,6 +186,7 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
99
186
|
}
|
|
100
187
|
break;
|
|
101
188
|
|
|
189
|
+
// Text search fields - check for Unicode
|
|
102
190
|
case 'BCC':
|
|
103
191
|
case 'BODY':
|
|
104
192
|
case 'CC':
|
|
@@ -114,28 +202,34 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
114
202
|
}
|
|
115
203
|
break;
|
|
116
204
|
|
|
205
|
+
// UID sequences
|
|
117
206
|
case 'UID':
|
|
118
207
|
if (params[term]) {
|
|
119
208
|
setOpt(attributes, term, params[term], 'SEQUENCE');
|
|
120
209
|
}
|
|
121
210
|
break;
|
|
122
211
|
|
|
212
|
+
// Email ID support (OBJECTID or Gmail extension)
|
|
123
213
|
case 'EMAILID':
|
|
124
214
|
if (connection.capabilities.has('OBJECTID')) {
|
|
125
215
|
setOpt(attributes, 'EMAILID', params[term]);
|
|
126
216
|
} else if (connection.capabilities.has('X-GM-EXT-1')) {
|
|
217
|
+
// Fallback to Gmail message ID
|
|
127
218
|
setOpt(attributes, 'X-GM-MSGID', params[term]);
|
|
128
219
|
}
|
|
129
220
|
break;
|
|
130
221
|
|
|
222
|
+
// Thread ID support (OBJECTID or Gmail extension)
|
|
131
223
|
case 'THREADID':
|
|
132
224
|
if (connection.capabilities.has('OBJECTID')) {
|
|
133
225
|
setOpt(attributes, 'THREADID', params[term]);
|
|
134
226
|
} else if (connection.capabilities.has('X-GM-EXT-1')) {
|
|
227
|
+
// Fallback to Gmail thread ID
|
|
135
228
|
setOpt(attributes, 'X-GM-THRID', params[term]);
|
|
136
229
|
}
|
|
137
230
|
break;
|
|
138
231
|
|
|
232
|
+
// Gmail raw search
|
|
139
233
|
case 'GMRAW':
|
|
140
234
|
case 'GMAILRAW': // alias for GMRAW
|
|
141
235
|
if (connection.capabilities.has('X-GM-EXT-1')) {
|
|
@@ -150,33 +244,65 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
150
244
|
}
|
|
151
245
|
break;
|
|
152
246
|
|
|
247
|
+
// Date searches with WITHIN extension support
|
|
153
248
|
case 'BEFORE':
|
|
154
|
-
case 'ON':
|
|
155
249
|
case 'SINCE':
|
|
250
|
+
{
|
|
251
|
+
// Use WITHIN extension for better timezone handling if available
|
|
252
|
+
if (connection.capabilities.has('WITHIN') && isDate(params[term])) {
|
|
253
|
+
// Convert to seconds ago from now
|
|
254
|
+
const now = Date.now();
|
|
255
|
+
const withinSeconds = Math.round(Math.max(0, now - params[term].getTime()) / 1000);
|
|
256
|
+
let withinKeyword;
|
|
257
|
+
switch (term.toUpperCase()) {
|
|
258
|
+
case 'BEFORE':
|
|
259
|
+
withinKeyword = 'OLDER';
|
|
260
|
+
break;
|
|
261
|
+
case 'SINCE':
|
|
262
|
+
withinKeyword = 'YOUNGER';
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
setOpt(attributes, withinKeyword, withinSeconds.toString());
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Fallback to standard date search
|
|
270
|
+
processDateField(attributes, term, params[term]);
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
|
|
274
|
+
// Standard date searches
|
|
275
|
+
case 'ON':
|
|
156
276
|
case 'SENTBEFORE':
|
|
157
277
|
case 'SENTON':
|
|
158
278
|
case 'SENTSINCE':
|
|
159
279
|
processDateField(attributes, term, params[term]);
|
|
160
280
|
break;
|
|
161
281
|
|
|
282
|
+
// Keyword/flag searches
|
|
162
283
|
case 'KEYWORD':
|
|
163
284
|
case 'UNKEYWORD':
|
|
164
285
|
{
|
|
165
286
|
let flag = formatFlag(params[term]);
|
|
287
|
+
// Only add if flag is supported or already exists in mailbox
|
|
166
288
|
if (canUseFlag(mailbox, flag) || mailbox.flags.has(flag)) {
|
|
167
289
|
setOpt(attributes, term, flag);
|
|
168
290
|
}
|
|
169
291
|
}
|
|
170
292
|
break;
|
|
171
293
|
|
|
294
|
+
// Header field searches
|
|
172
295
|
case 'HEADER':
|
|
173
296
|
if (params[term] && typeof params[term] === 'object') {
|
|
174
297
|
Object.keys(params[term]).forEach(header => {
|
|
175
298
|
let value = params[term][header];
|
|
299
|
+
|
|
300
|
+
// Allow boolean true to search for header existence
|
|
176
301
|
if (value === true) {
|
|
177
302
|
value = '';
|
|
178
303
|
}
|
|
179
304
|
|
|
305
|
+
// Skip non-string values (after true->'' conversion)
|
|
180
306
|
if (typeof value !== 'string') {
|
|
181
307
|
return;
|
|
182
308
|
}
|
|
@@ -190,25 +316,29 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
190
316
|
}
|
|
191
317
|
break;
|
|
192
318
|
|
|
319
|
+
// NOT operator
|
|
193
320
|
case 'NOT':
|
|
194
|
-
{
|
|
321
|
+
{
|
|
195
322
|
if (!params[term]) {
|
|
196
323
|
break;
|
|
197
324
|
}
|
|
198
325
|
|
|
199
326
|
if (typeof params[term] === 'object') {
|
|
200
327
|
attributes.push({ type: 'ATOM', value: 'NOT' });
|
|
328
|
+
// Recursively process NOT conditions
|
|
201
329
|
walk(params[term]);
|
|
202
330
|
}
|
|
203
331
|
}
|
|
204
332
|
break;
|
|
205
|
-
|
|
333
|
+
|
|
334
|
+
// OR operator - complex logic for building OR trees
|
|
206
335
|
case 'OR':
|
|
207
336
|
{
|
|
208
337
|
if (!params[term] || !Array.isArray(params[term]) || !params[term].length) {
|
|
209
338
|
break;
|
|
210
339
|
}
|
|
211
340
|
|
|
341
|
+
// Single element - just process it directly
|
|
212
342
|
if (params[term].length === 1) {
|
|
213
343
|
if (typeof params[term][0] === 'object' && params[term][0]) {
|
|
214
344
|
walk(params[term][0]);
|
|
@@ -216,12 +346,18 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
216
346
|
break;
|
|
217
347
|
}
|
|
218
348
|
|
|
219
|
-
|
|
220
|
-
|
|
349
|
+
/**
|
|
350
|
+
* Generates a binary tree structure for OR operations.
|
|
351
|
+
* IMAP OR takes exactly 2 operands, so we need to nest them.
|
|
352
|
+
*
|
|
353
|
+
* @param {Array} list - List of conditions to OR together
|
|
354
|
+
* @returns {Array} Binary tree structure
|
|
355
|
+
*/
|
|
221
356
|
let genOrTree = list => {
|
|
222
357
|
let group = false;
|
|
223
358
|
let groups = [];
|
|
224
359
|
|
|
360
|
+
// Group items in pairs
|
|
225
361
|
list.forEach((entry, i) => {
|
|
226
362
|
if (i % 2 === 0) {
|
|
227
363
|
group = [entry];
|
|
@@ -232,6 +368,7 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
232
368
|
}
|
|
233
369
|
});
|
|
234
370
|
|
|
371
|
+
// Handle odd number of items
|
|
235
372
|
if (group && group.length) {
|
|
236
373
|
while (group.length === 1 && Array.isArray(group[0])) {
|
|
237
374
|
group = group[0];
|
|
@@ -240,10 +377,12 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
240
377
|
groups.push(group);
|
|
241
378
|
}
|
|
242
379
|
|
|
380
|
+
// Recursively group until we have a binary tree
|
|
243
381
|
while (groups.length > 2) {
|
|
244
382
|
groups = genOrTree(groups);
|
|
245
383
|
}
|
|
246
384
|
|
|
385
|
+
// Flatten single-element arrays
|
|
247
386
|
while (groups.length === 1 && Array.isArray(groups[0])) {
|
|
248
387
|
groups = groups[0];
|
|
249
388
|
}
|
|
@@ -251,8 +390,13 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
251
390
|
return groups;
|
|
252
391
|
};
|
|
253
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Walks the OR tree and generates IMAP commands.
|
|
395
|
+
* @param {Array|Object} entry - Tree node to process
|
|
396
|
+
*/
|
|
254
397
|
let walkOrTree = entry => {
|
|
255
398
|
if (Array.isArray(entry)) {
|
|
399
|
+
// Only add OR for multiple items
|
|
256
400
|
if (entry.length > 1) {
|
|
257
401
|
attributes.push({ type: 'ATOM', value: 'OR' });
|
|
258
402
|
}
|
|
@@ -263,6 +407,7 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
263
407
|
walk(entry);
|
|
264
408
|
}
|
|
265
409
|
};
|
|
410
|
+
|
|
266
411
|
walkOrTree(genOrTree(params[term]));
|
|
267
412
|
}
|
|
268
413
|
break;
|
|
@@ -270,10 +415,12 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
270
415
|
});
|
|
271
416
|
};
|
|
272
417
|
|
|
418
|
+
// Process the query
|
|
273
419
|
walk(query);
|
|
274
420
|
|
|
421
|
+
// If we encountered Unicode strings and UTF-8 is not already accepted,
|
|
422
|
+
// prepend CHARSET UTF-8 to the search command
|
|
275
423
|
if (hasUnicode && !connection.enabled.has('UTF8=ACCEPT')) {
|
|
276
|
-
// Prepend search query with `CHARSET UTF-8`
|
|
277
424
|
attributes.unshift({ type: 'ATOM', value: 'UTF-8' });
|
|
278
425
|
attributes.unshift({ type: 'ATOM', value: 'CHARSET' });
|
|
279
426
|
}
|
package/lib/tools.js
CHANGED
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.190",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "lib/imap-flow.js",
|
|
6
|
+
"types": "lib/imap-flow.d.ts",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"test": "grunt",
|
|
8
9
|
"prepare": "npm run build",
|
|
9
10
|
"docs": "rm -rf docs && mkdir -p docs && jsdoc lib/imap-flow.js -c jsdoc.json -R README.md --destination docs/ && cp assets/favicon.ico docs",
|
|
10
|
-
"
|
|
11
|
-
"build": "npm run docs && npm run dst",
|
|
11
|
+
"build": "npm run docs",
|
|
12
12
|
"st": "npm run docs && st -d docs -i index.html",
|
|
13
13
|
"update": "rm -rf node_modules package-lock.json && ncu -u && npm install"
|
|
14
14
|
},
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://imapflow.com/",
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@babel/eslint-parser": "7.
|
|
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.
|
|
35
|
-
"@types/node": "24.0.
|
|
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",
|
|
@@ -43,16 +43,16 @@
|
|
|
43
43
|
"imapflow-jsdoc-template": "3.4.0-imapflow.2",
|
|
44
44
|
"jsdoc": "3.6.11",
|
|
45
45
|
"st": "3.0.2",
|
|
46
|
-
"
|
|
46
|
+
"typescript": "5.8.3"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"encoding-japanese": "2.2.0",
|
|
50
50
|
"iconv-lite": "0.6.3",
|
|
51
51
|
"libbase64": "1.3.0",
|
|
52
|
-
"libmime": "5.3.
|
|
52
|
+
"libmime": "5.3.7",
|
|
53
53
|
"libqp": "2.1.1",
|
|
54
|
-
"mailsplit": "5.4.
|
|
55
|
-
"nodemailer": "7.0.
|
|
54
|
+
"mailsplit": "5.4.5",
|
|
55
|
+
"nodemailer": "7.0.5",
|
|
56
56
|
"pino": "9.7.0",
|
|
57
57
|
"socks": "2.8.5"
|
|
58
58
|
}
|