imapflow 2.0.8 → 2.1.1

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.
@@ -39,14 +39,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  return (mod && mod.__esModule) ? mod : { "default": mod };
40
40
  };
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.ImapFlow = exports.AuthenticationFailure = void 0;
42
+ exports.ImapFlow = exports.ImapFlowErrorCode = exports.AuthenticationFailure = void 0;
43
43
  const node_tls_1 = __importDefault(require("node:tls"));
44
44
  const node_net_1 = __importDefault(require("node:net"));
45
45
  const node_crypto_1 = __importDefault(require("node:crypto"));
46
46
  const node_zlib_1 = __importDefault(require("node:zlib"));
47
47
  const node_events_1 = require("node:events");
48
48
  const node_stream_1 = require("node:stream");
49
- const logger_js_1 = __importDefault(require("./logger.js"));
49
+ const logger_js_1 = require("./logger.js");
50
50
  const packageInfo = __importStar(require("./package-info.js"));
51
51
  const imap_stream_js_1 = require("./handler/imap-stream.js");
52
52
  const imap_handler_js_1 = require("./handler/imap-handler.js");
@@ -58,6 +58,7 @@ const imap_commands_js_1 = __importDefault(require("./imap-commands.js"));
58
58
  const tools_js_1 = require("./tools.js");
59
59
  var errors_js_2 = require("./errors.js");
60
60
  Object.defineProperty(exports, "AuthenticationFailure", { enumerable: true, get: function () { return errors_js_2.AuthenticationFailure; } });
61
+ Object.defineProperty(exports, "ImapFlowErrorCode", { enumerable: true, get: function () { return errors_js_2.ImapFlowErrorCode; } });
61
62
  const GREETING_TIMEOUT = 16 * 1000;
62
63
  const UPGRADE_TIMEOUT = 10 * 1000;
63
64
  const SOCKET_TIMEOUT = 5 * 60 * 1000;
@@ -1601,6 +1602,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1601
1602
  /** @internal */
1602
1603
  beginSession(onUnhandledError) {
1603
1604
  (0, tools_js_1.clearTimer)(this.greetingTimeout);
1605
+ this.greetingReceived = true;
1604
1606
  this.untaggedHandlers.OK = null;
1605
1607
  this.untaggedHandlers.PREAUTH = null;
1606
1608
  if (this.isClosed) {
@@ -1660,6 +1662,12 @@ class ImapFlow extends node_events_1.EventEmitter {
1660
1662
  this.byeReason = reason || 'Server closed connection';
1661
1663
  this.untaggedHandlers.BYE = null;
1662
1664
  this.state = this.states.LOGOUT;
1665
+ // A BYE greeting rejects the connection outright. Do not wait for the server to close
1666
+ // the socket: one that keeps it open would leave connect() pending until the greeting
1667
+ // timeout.
1668
+ if (!this.greetingReceived) {
1669
+ this.closeAfter();
1670
+ }
1663
1671
  }
1664
1672
  // Drops every capability-derived field together - the counterpart of
1665
1673
  // updateCapabilitiesFromRaw() below, which sets them together. rawCapabilities is
@@ -1892,7 +1900,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1892
1900
  /** @internal */
1893
1901
  autoidle() {
1894
1902
  (0, tools_js_1.clearTimer)(this.idleStartTimer);
1895
- if (this.options.disableAutoIdle || this.state !== this.states.SELECTED) {
1903
+ if (this.options.disableAutoIdle || !this.usable || this.state !== this.states.SELECTED) {
1896
1904
  return;
1897
1905
  }
1898
1906
  if (this.connectionBusy()) {
@@ -1904,7 +1912,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1904
1912
  // missed clearTimeout would inject IDLE between a caller's own commands. Declining
1905
1913
  // postpones rather than cancels: whatever made the connection busy calls autoidle()
1906
1914
  // again when it finishes.
1907
- if (this.state !== this.states.SELECTED || this.connectionBusy()) {
1915
+ if (!this.usable || this.state !== this.states.SELECTED || this.connectionBusy()) {
1908
1916
  return;
1909
1917
  }
1910
1918
  this.idle().catch(err => (0, tools_js_1.logConnectionError)(this, 'Auto-IDLE failed', err));
@@ -1923,9 +1931,17 @@ class ImapFlow extends node_events_1.EventEmitter {
1923
1931
  async connect() {
1924
1932
  if (this._connectCalled) {
1925
1933
  // Prevent re-using ImapFlow instances by allowing to call connect just once.
1926
- throw new Error('Can not re-use ImapFlow instance');
1934
+ let err = new Error('Can not re-use ImapFlow instance');
1935
+ err.code = 'InstanceReused';
1936
+ throw err;
1927
1937
  }
1928
1938
  this._connectCalled = true;
1939
+ let closedError = () => this.createNoConnectionError(this.byeReason, { rejectedFrom: 'connect' });
1940
+ // close() already ran, so there is no connection to set up and nothing would ever
1941
+ // settle a connect attempt started now.
1942
+ if (this.isClosed) {
1943
+ throw closedError();
1944
+ }
1929
1945
  // One deadline for the whole attempt, started before anything is resolved or negotiated.
1930
1946
  // Proxy DNS and proxy negotiation used to run entirely outside the timer, so a stalled
1931
1947
  // proxy could hang far beyond the documented connectionTimeout.
@@ -1971,9 +1987,20 @@ class ImapFlow extends node_events_1.EventEmitter {
1971
1987
  error._err = err;
1972
1988
  throw error;
1973
1989
  }
1990
+ // close() during proxy setup found no socket to destroy and no connect to reject,
1991
+ // so the tunnel that just opened is dropped here instead of being handed to a
1992
+ // closed client.
1993
+ if (this.isClosed) {
1994
+ socket.destroy();
1995
+ throw closedError();
1996
+ }
1974
1997
  }
1975
1998
  // Guarded: close() rejects a pending connect() synchronously. See guardedPromise().
1976
1999
  let connectPromise = (0, tools_js_1.guardedPromise)((resolve, reject) => {
2000
+ // Stored before the transport is up, so a close() that lands before onConnect
2001
+ // still rejects this attempt through closeConnectSteps().
2002
+ this.initialResolve = resolve;
2003
+ this.initialReject = reject;
1977
2004
  // Whatever the proxy phase already used is gone from the budget
1978
2005
  this.connectTimeout = setTimeout(() => {
1979
2006
  let err = deadline.error();
@@ -2025,9 +2052,6 @@ class ImapFlow extends node_events_1.EventEmitter {
2025
2052
  this.setSocketHandlers();
2026
2053
  this.setEventHandlers();
2027
2054
  connected.pipe(this.streamer);
2028
- // executed by initial "* OK"
2029
- this.initialResolve = resolve;
2030
- this.initialReject = reject;
2031
2055
  /* c8 ignore next 4 */ // defensive: the onConnect setup body does not throw under normal operation
2032
2056
  }
2033
2057
  catch (ex) {
@@ -2211,7 +2235,10 @@ class ImapFlow extends node_events_1.EventEmitter {
2211
2235
  this._upgradeReject = null;
2212
2236
  reject(this.createNoConnectionError(false, { rejectedFrom: 'upgrade' }));
2213
2237
  }
2214
- if (typeof this.initialReject === 'function' && !this.options.verifyOnly) {
2238
+ // A verifyOnly session closes itself with LOGOUT and startSession() settles connect()
2239
+ // with the outcome, so the close is not a failure there. Before the greeting nothing
2240
+ // else can settle it (BYE greeting, FIN, close() while the transport is still coming up).
2241
+ if (typeof this.initialReject === 'function' && (!this.options.verifyOnly || !this.greetingReceived)) {
2215
2242
  (0, tools_js_1.clearTimer)(this.greetingTimeout);
2216
2243
  let reject = this.initialReject;
2217
2244
  this.initialResolve = false;
@@ -2522,7 +2549,26 @@ class ImapFlow extends node_events_1.EventEmitter {
2522
2549
  * // 125
2523
2550
  */
2524
2551
  async mailboxOpen(path, options) {
2525
- return await this.run('SELECT', path, options);
2552
+ try {
2553
+ return await this.run('SELECT', path, options);
2554
+ }
2555
+ catch (err) {
2556
+ if (err.responseStatus === 'NO') {
2557
+ // SELECT failed with NO: verify whether the mailbox exists at all by running
2558
+ // LIST. This sets mailboxMissing on the error so the caller can distinguish
2559
+ // "doesn't exist" from other failures, for getMailboxLock() callers as well.
2560
+ try {
2561
+ let folders = await this.run('LIST', '', (0, tools_js_1.normalizePath)(this, path), { listOnly: true });
2562
+ if (!folders || !folders.length) {
2563
+ err.mailboxMissing = true;
2564
+ }
2565
+ }
2566
+ catch (E) {
2567
+ this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
2568
+ }
2569
+ }
2570
+ throw err;
2571
+ }
2526
2572
  }
2527
2573
  /**
2528
2574
  * Closes a previously opened mailbox
@@ -3224,20 +3270,7 @@ class ImapFlow extends node_events_1.EventEmitter {
3224
3270
  break; // Wait for this lock to be released
3225
3271
  }
3226
3272
  catch (err) {
3227
- if (err.responseStatus === 'NO') {
3228
- // SELECT failed with NO: verify whether the mailbox exists
3229
- // at all by running LIST. This sets mailboxMissing on the error
3230
- // so the caller can distinguish "doesn't exist" from other failures.
3231
- try {
3232
- let folders = await this.run('LIST', '', path, { listOnly: true });
3233
- if (!folders || !folders.length) {
3234
- err.mailboxMissing = true;
3235
- }
3236
- }
3237
- catch (E) {
3238
- this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
3239
- }
3240
- }
3273
+ // mailboxOpen() has already marked a missing mailbox (mailboxMissing)
3241
3274
  this.log.trace({
3242
3275
  msg: 'Failed to acquire mailbox lock',
3243
3276
  path,
@@ -3328,12 +3361,14 @@ class ImapFlow extends node_events_1.EventEmitter {
3328
3361
  }
3329
3362
  /** @internal */
3330
3363
  getLogger() {
3331
- let mainLogger = this.options.logger && typeof this.options.logger === 'object'
3332
- ? this.options.logger
3333
- : logger_js_1.default.child({
3334
- component: 'imap-connection',
3335
- cid: this.id
3336
- });
3364
+ let mainLogger = {};
3365
+ if (this.options.logger && typeof this.options.logger === 'object') {
3366
+ mainLogger = this.options.logger;
3367
+ }
3368
+ else if (this.options.logger !== false) {
3369
+ // {logger:false} never consults mainLogger, so it does not create the default logger
3370
+ mainLogger = (0, logger_js_1.createConnectionLogger)({ cid: this.id, logRaw: this.options.logRaw });
3371
+ }
3337
3372
  let synteticLogger = {};
3338
3373
  let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
3339
3374
  for (let level of levels) {
@@ -3389,7 +3424,8 @@ class ImapFlow extends node_events_1.EventEmitter {
3389
3424
  }
3390
3425
  /**
3391
3426
  * Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
3392
- * (e.g., STARTTLS) or transferring socket ownership.
3427
+ * (e.g., STARTTLS) or transferring socket ownership. Call it while the connection is not
3428
+ * idling: an IDLE in progress is not broken first, so the server still expects `DONE`.
3393
3429
  *
3394
3430
  * @returns Socket objects: `readSocket` is the read socket (inflated socket if compression is enabled, raw socket otherwise),
3395
3431
  * `writeSocket` the write socket and `socket` the raw underlying socket (same as readSocket/writeSocket when compression is disabled)
@@ -3404,6 +3440,11 @@ class ImapFlow extends node_events_1.EventEmitter {
3404
3440
  // compression is active, the PassThrough writeSocket - so the connection
3405
3441
  // is fully released to the caller.
3406
3442
  this.clearSocketHandlers();
3443
+ // The socket now belongs to the caller. Marking the client unusable keeps
3444
+ // auto-IDLE (armed now, or re-armed by a lock release or a finished download)
3445
+ // and a later dispose from writing IDLE or LOGOUT onto it.
3446
+ this.usable = false;
3447
+ (0, tools_js_1.clearTimer)(this.idleStartTimer);
3407
3448
  const readSocket = this._inflate || socket;
3408
3449
  const writeSocket = this.writeSocket || socket;
3409
3450
  // Defense-in-depth: when compression is active the raw socket is orphaned
@@ -3502,6 +3543,23 @@ exports.ImapFlow = ImapFlow;
3502
3543
  * console.log(`${entry.cid} ${entry.msg}`);
3503
3544
  * });
3504
3545
  */
3546
+ // Installed outside the class body: a computed `[Symbol.asyncDispose]` key would turn into a
3547
+ // method named "undefined" on Node.js 20.0-20.3, which predate the symbol
3548
+ if (typeof Symbol.asyncDispose === 'symbol') {
3549
+ ImapFlow.prototype[Symbol.asyncDispose] = async function () {
3550
+ if (this.usable) {
3551
+ try {
3552
+ await this.logout();
3553
+ }
3554
+ catch (err) {
3555
+ (0, tools_js_1.logConnectionError)(this, 'LOGOUT failed while disposing', err);
3556
+ }
3557
+ }
3558
+ // logout() already closes the connection; close() is idempotent and covers a client
3559
+ // that never connected or whose logout was skipped
3560
+ this.close();
3561
+ };
3562
+ }
3505
3563
  // Both `import { ImapFlow } from 'imapflow'` and `import imapflow from 'imapflow'` work, the
3506
3564
  // latter matching the shape `require('imapflow')` has always had
3507
3565
  const imapflow = { ImapFlow, AuthenticationFailure: errors_js_1.AuthenticationFailure };
@@ -1,3 +1,21 @@
1
- import pino from 'pino';
2
- declare const logger: pino.Logger<never, boolean>;
3
- export default logger;
1
+ import { type Logger as PinoLogger } from 'pino';
2
+ /**
3
+ * Returns the shared default logger, used when a connection is given no logger of its own.
4
+ * Created on first use, so importing the library does not set up a stdout logger nobody asked
5
+ * for. Level info: a caller that asks for raw socket data (logRaw) lowers its own child logger
6
+ * to trace.
7
+ *
8
+ * @returns The default pino logger
9
+ */
10
+ export declare function getDefaultLogger(): PinoLogger;
11
+ /**
12
+ * Returns the child of the default logger a connection logs through when it was given no
13
+ * logger of its own.
14
+ *
15
+ * @param options - cid is the connection id to stamp on entries, logRaw asks for raw socket data
16
+ * @returns A child logger of the default logger
17
+ */
18
+ export declare function createConnectionLogger(options: {
19
+ cid?: string | undefined;
20
+ logRaw?: boolean | undefined;
21
+ }): PinoLogger;
@@ -3,9 +3,39 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getDefaultLogger = getDefaultLogger;
7
+ exports.createConnectionLogger = createConnectionLogger;
6
8
  const pino_1 = __importDefault(require("pino"));
7
- const logger = (0, pino_1.default)();
8
- logger.level = 'trace';
9
- exports.default = logger;
10
- module.exports = exports.default;
11
- Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
9
+ let logger = null;
10
+ /**
11
+ * Returns the shared default logger, used when a connection is given no logger of its own.
12
+ * Created on first use, so importing the library does not set up a stdout logger nobody asked
13
+ * for. Level info: a caller that asks for raw socket data (logRaw) lowers its own child logger
14
+ * to trace.
15
+ *
16
+ * @returns The default pino logger
17
+ */
18
+ function getDefaultLogger() {
19
+ if (!logger) {
20
+ logger = (0, pino_1.default)({ level: 'info' });
21
+ }
22
+ return logger;
23
+ }
24
+ /**
25
+ * Returns the child of the default logger a connection logs through when it was given no
26
+ * logger of its own.
27
+ *
28
+ * @param options - cid is the connection id to stamp on entries, logRaw asks for raw socket data
29
+ * @returns A child logger of the default logger
30
+ */
31
+ function createConnectionLogger(options) {
32
+ let child = getDefaultLogger().child({
33
+ component: 'imap-connection',
34
+ cid: options.cid
35
+ });
36
+ // Raw socket data is logged at trace level, so asking for it lowers the threshold
37
+ if (options.logRaw) {
38
+ child.level = 'trace';
39
+ }
40
+ return child;
41
+ }
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.0.8";
2
+ export declare const version = "2.1.1";
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.0.8";
6
+ exports.version = "2.1.1";
7
7
  exports.homepage = "https://imapflow.com/";
@@ -9,7 +9,7 @@ export type SearchAttribute = ImapAttributeNode | SearchAttribute[];
9
9
  * Compiles a JavaScript object query into IMAP search command attributes.
10
10
  * Supports standard IMAP search criteria and extensions like OBJECTID and Gmail extensions.
11
11
  *
12
- * @param connection - IMAP connection object (capabilities, enabled extensions and the current mailbox are read)
12
+ * @param connection - IMAP connection object (capabilities and enabled extensions are read)
13
13
  * @param query - Search query object
14
14
  * @returns Array of IMAP search attributes
15
15
  * @throws {Error} When required server extensions are not available
@@ -91,6 +91,17 @@ let processDateField = (attributes, term, value) => {
91
91
  };
92
92
  // Pre-compiled regex for better performance
93
93
  const UNICODE_PATTERN = /[^\x00-\x7F]/;
94
+ /**
95
+ * Throws a coded search compilation error.
96
+ *
97
+ * @param code - Error code, one of the ImapFlowErrorCode values
98
+ * @param message - Error message
99
+ */
100
+ let fail = (code, message) => {
101
+ let error = new Error(message);
102
+ error.code = code;
103
+ throw error;
104
+ };
94
105
  /**
95
106
  * Checks if a string contains Unicode characters.
96
107
  * Used to determine if CHARSET UTF-8 needs to be specified.
@@ -110,7 +121,7 @@ let isUnicodeString = (str) => {
110
121
  * Compiles a JavaScript object query into IMAP search command attributes.
111
122
  * Supports standard IMAP search criteria and extensions like OBJECTID and Gmail extensions.
112
123
  *
113
- * @param connection - IMAP connection object (capabilities, enabled extensions and the current mailbox are read)
124
+ * @param connection - IMAP connection object (capabilities and enabled extensions are read)
114
125
  * @param query - Search query object
115
126
  * @returns Array of IMAP search attributes
116
127
  * @throws {Error} When required server extensions are not available
@@ -136,21 +147,29 @@ const searchCompiler = (connection, query) => {
136
147
  const attributes = [];
137
148
  // Track if we need to specify UTF-8 charset
138
149
  let hasUnicode = false;
139
- const mailbox = connection.mailbox;
140
150
  /**
141
151
  * Recursively walks through the query object and builds IMAP attributes.
142
152
  * @param params - Query parameters to process
143
153
  */
144
154
  const walk = (params) => {
145
- // Walks a query object and wraps the resulting attributes in a
146
- // sub-array so the IMAP compiler emits parentheses around them.
147
- // Used when a single search-key is required (NOT, OR operands)
148
- // but the condition has multiple keys (RFC 3501 Section 6.4.4).
149
- let walkGrouped = (obj) => {
155
+ // Compiles one NOT or OR operand, which the caller has already put its operator in
156
+ // front of. An operand with several keys is wrapped in a sub-array so the IMAP compiler
157
+ // emits parentheses around it, as the operator takes a single search-key
158
+ // (RFC 3501 Section 6.4.4). An operand that compiles to nothing (an invalid date, an
159
+ // empty object, ...) is refused: the operator would otherwise bind to whatever
160
+ // criterion follows it and invert or widen the search.
161
+ let walkOperand = (operator, obj) => {
150
162
  let startIdx = attributes.length;
151
- walk(obj);
152
- let subAttrs = attributes.splice(startIdx);
153
- attributes.push(subAttrs);
163
+ if (obj && typeof obj === 'object') {
164
+ walk(obj);
165
+ }
166
+ if (attributes.length === startIdx) {
167
+ fail('InvalidSearchQuery', `Search operand for ${operator} does not include any usable search criteria`);
168
+ }
169
+ if (Object.keys(obj).length > 1) {
170
+ let subAttrs = attributes.splice(startIdx);
171
+ attributes.push(subAttrs);
172
+ }
154
173
  };
155
174
  Object.keys(params || {}).forEach(term => {
156
175
  switch (term.toUpperCase()) {
@@ -196,9 +215,7 @@ const searchCompiler = (connection, query) => {
196
215
  // reject the whole search with a tagged BAD, so fail with a
197
216
  // descriptive error instead
198
217
  if ((0, tools_js_1.isRev2Active)(connection)) {
199
- let error = new Error(`The "${term.toLowerCase()}" search key does not exist in IMAP4rev2`);
200
- error.code = 'MissingServerExtension';
201
- throw error;
218
+ fail('MissingServerExtension', `The "${term.toLowerCase()}" search key does not exist in IMAP4rev2`);
202
219
  }
203
220
  setBoolOpt(attributes, term, true);
204
221
  }
@@ -244,6 +261,11 @@ const searchCompiler = (connection, query) => {
244
261
  // Fallback to Gmail message ID
245
262
  setOpt(attributes, 'X-GM-MSGID', params[term]);
246
263
  }
264
+ else if (params[term]) {
265
+ // Dropping the criterion would widen the search to every message
266
+ // matching the rest of the query, which a delete or move acts on
267
+ fail('MissingServerExtension', 'Server does not support OBJECTID or X-GM-EXT-1 extension required for EMAILID');
268
+ }
247
269
  break;
248
270
  // Thread ID support (OBJECTID or Gmail extension)
249
271
  case 'THREADID':
@@ -254,6 +276,11 @@ const searchCompiler = (connection, query) => {
254
276
  // Fallback to Gmail thread ID
255
277
  setOpt(attributes, 'X-GM-THRID', params[term]);
256
278
  }
279
+ else if (params[term]) {
280
+ // Dropping the criterion would widen the search to every message
281
+ // matching the rest of the query, which a delete or move acts on
282
+ fail('MissingServerExtension', 'Server does not support OBJECTID or X-GM-EXT-1 extension required for THREADID');
283
+ }
257
284
  break;
258
285
  // Gmail raw search
259
286
  case 'GMRAW':
@@ -265,9 +292,7 @@ const searchCompiler = (connection, query) => {
265
292
  setOpt(attributes, 'X-GM-RAW', params[term]);
266
293
  }
267
294
  else {
268
- let error = new Error('Server does not support X-GM-EXT-1 extension required for X-GM-RAW');
269
- error.code = 'MissingServerExtension';
270
- throw error;
295
+ fail('MissingServerExtension', 'Server does not support X-GM-EXT-1 extension required for X-GM-RAW');
271
296
  }
272
297
  break;
273
298
  // Gmail label search. Compiles { has, not } into an X-GM-RAW "label:"/"-label:" query
@@ -301,9 +326,7 @@ const searchCompiler = (connection, query) => {
301
326
  break;
302
327
  }
303
328
  if (!connection.capabilities.has('X-GM-EXT-1')) {
304
- let error = new Error('Server does not support X-GM-EXT-1 extension required for label search');
305
- error.code = 'MissingServerExtension';
306
- throw error;
329
+ fail('MissingServerExtension', 'Server does not support X-GM-EXT-1 extension required for label search');
307
330
  }
308
331
  let rawQuery = rawParts.join(' ');
309
332
  if (isUnicodeString(rawQuery)) {
@@ -347,8 +370,10 @@ const searchCompiler = (connection, query) => {
347
370
  case 'UNKEYWORD':
348
371
  {
349
372
  let flag = (0, tools_js_1.formatFlag)(params[term]);
350
- // Only add if flag is supported or already exists in mailbox
351
- if ((0, tools_js_1.canUseFlag)(mailbox, flag) || mailbox.flags.has(flag)) {
373
+ // Compiled even when the mailbox does not allow the keyword: the
374
+ // correct answer is then the empty set, which dropping the
375
+ // criterion would turn into every message matching the rest
376
+ if (flag) {
352
377
  setOpt(attributes, term, flag);
353
378
  }
354
379
  }
@@ -377,12 +402,7 @@ const searchCompiler = (connection, query) => {
377
402
  case 'NOT':
378
403
  if (params[term] && typeof params[term] === 'object') {
379
404
  attributes.push({ type: 'ATOM', value: 'NOT' });
380
- if (Object.keys(params[term]).length > 1) {
381
- walkGrouped(params[term]);
382
- }
383
- else {
384
- walk(params[term]);
385
- }
405
+ walkOperand('NOT', params[term]);
386
406
  }
387
407
  break;
388
408
  // OR operator - complex logic for building OR trees
@@ -441,14 +461,7 @@ const searchCompiler = (connection, query) => {
441
461
  entry.forEach(walkOrTree);
442
462
  return;
443
463
  }
444
- if (entry && typeof entry === 'object') {
445
- if (Object.keys(entry).length > 1) {
446
- walkGrouped(entry);
447
- }
448
- else {
449
- walk(entry);
450
- }
451
- }
464
+ walkOperand('OR', entry);
452
465
  };
453
466
  walkOrTree(genOrTree(params[term]));
454
467
  }
@@ -1,6 +1,6 @@
1
1
  import type { Transform } from 'node:stream';
2
2
  import type { ImapFlow } from './imap-flow.js';
3
- import type { ConnectionErrorSite, ImapFlowError } from './errors.js';
3
+ import { type ConnectionErrorSite, type ImapFlowError } from './errors.js';
4
4
  import type { ImapAttributeList, ImapAttributeNode, ImapResponse } from './handler/types.js';
5
5
  import type { FetchMessageObject, ListResponse, ListTreeResponse, MailboxObject, MessageEnvelopeObject, MessageStructureObject, ImapFlowEvents, StatusQuery } from './types.js';
6
6
  export { AuthenticationFailure } from './errors.js';
package/dist/cjs/tools.js CHANGED
@@ -58,11 +58,12 @@ const imap_handler_js_1 = require("./handler/imap-handler.js");
58
58
  const node_crypto_1 = require("node:crypto");
59
59
  const jp_decoder_js_1 = require("./jp-decoder.js");
60
60
  const iconv_lite_1 = __importDefault(require("iconv-lite"));
61
- var errors_js_1 = require("./errors.js");
62
- Object.defineProperty(exports, "AuthenticationFailure", { enumerable: true, get: function () { return errors_js_1.AuthenticationFailure; } });
61
+ const errors_js_1 = require("./errors.js");
62
+ var errors_js_2 = require("./errors.js");
63
+ Object.defineProperty(exports, "AuthenticationFailure", { enumerable: true, get: function () { return errors_js_2.AuthenticationFailure; } });
63
64
  const FLAG_COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];
64
65
  // Error codes that only mean the connection is no longer usable. See logConnectionError().
65
- const CONNECTION_GONE_CODES = new Set(['NoConnection', 'EConnectionClosed', 'StateLogout']);
66
+ const CONNECTION_GONE_CODES = new Set([errors_js_1.ImapFlowErrorCode.NoConnection, errors_js_1.ImapFlowErrorCode.EConnectionClosed, errors_js_1.ImapFlowErrorCode.StateLogout]);
66
67
  // Upper bound for expanding server-supplied sequence ranges (see expandRange). 2^24
67
68
  // entries in total is far beyond any legitimate mailbox while keeping the worst-case
68
69
  // expansion of a hostile range set bounded.
@@ -815,11 +816,18 @@ async function formatMessageResponse(untagged, mailbox) {
815
816
  case 'flags':
816
817
  map.flags = new Set(getArray(attribute));
817
818
  break;
819
+ // A server with nothing to report can answer ENVELOPE NIL or BODYSTRUCTURE NIL.
820
+ // The field is left unset then: parsing NIL threw, and the whole message
821
+ // disappeared from the FETCH result.
818
822
  case 'envelope':
819
- map.envelope = parseEnvelope(attribute);
823
+ if (Array.isArray(attribute)) {
824
+ map.envelope = parseEnvelope(attribute);
825
+ }
820
826
  break;
821
827
  case 'bodystructure':
822
- map.bodyStructure = parseBodystructure(attribute);
828
+ if (Array.isArray(attribute)) {
829
+ map.bodyStructure = parseBodystructure(attribute);
830
+ }
823
831
  break;
824
832
  case 'internaldate': {
825
833
  let value = getString(attribute);
@@ -1147,7 +1155,7 @@ function parseBodystructure(entry) {
1147
1155
  curNode.type = 'multipart/' + ((node[i++] || {}).value || '').toString().toLowerCase();
1148
1156
  // extension data (not available for BODY requests)
1149
1157
  // body parameter parenthesized list
1150
- if (i < node.length - 1) {
1158
+ if (i < node.length) {
1151
1159
  if (node[i]) {
1152
1160
  curNode.parameters = getStructuredParams(node[i]);
1153
1161
  }
@@ -1229,7 +1237,7 @@ function parseBodystructure(entry) {
1229
1237
  }
1230
1238
  // extension data (not available for BODY requests)
1231
1239
  // md5
1232
- if (i < node.length - 1) {
1240
+ if (i < node.length) {
1233
1241
  if (node[i]) {
1234
1242
  curNode.md5 = (node[i].value || '').toString().toLowerCase();
1235
1243
  }
@@ -1239,7 +1247,7 @@ function parseBodystructure(entry) {
1239
1247
  // the following are shared extension values (for both multipart and non-multipart parts)
1240
1248
  // not available for BODY requests
1241
1249
  // body disposition
1242
- if (i < node.length - 1) {
1250
+ if (i < node.length) {
1243
1251
  let disposition = node[i];
1244
1252
  if (Array.isArray(disposition) && disposition.length) {
1245
1253
  curNode.disposition = ((disposition[0] && disposition[0].value) || '').toString().toLowerCase();
@@ -1250,7 +1258,7 @@ function parseBodystructure(entry) {
1250
1258
  i++;
1251
1259
  }
1252
1260
  // body language
1253
- if (i < node.length - 1) {
1261
+ if (i < node.length) {
1254
1262
  if (node[i]) {
1255
1263
  /* c8 ignore next */ // node[i] is truthy inside this guard, so the [] fallback is unreachable
1256
1264
  curNode.language = [].concat(node[i] || []).map(val => ((val && val.value) || '').toString().toLowerCase());
@@ -1260,7 +1268,7 @@ function parseBodystructure(entry) {
1260
1268
  // body location
1261
1269
  // NB! defined as a "string list" in RFC3501 but replaced in errata document with "string"
1262
1270
  // Errata: http://www.rfc-editor.org/errata_search.php?rfc=3501
1263
- if (i < node.length - 1) {
1271
+ if (i < node.length) {
1264
1272
  if (node[i]) {
1265
1273
  curNode.location = (node[i].value || '').toString();
1266
1274
  }