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.
@@ -7,7 +7,7 @@ import crypto from 'node:crypto';
7
7
  import zlib from 'node:zlib';
8
8
  import { EventEmitter } from 'node:events';
9
9
  import { PassThrough } from 'node:stream';
10
- import logger from './logger.js';
10
+ import { createConnectionLogger } from './logger.js';
11
11
  import * as packageInfo from './package-info.js';
12
12
  import { ImapStream } from './handler/imap-stream.js';
13
13
  import { parser, compiler } from './handler/imap-handler.js';
@@ -17,7 +17,7 @@ import { downloadMessage, downloadMessageParts } from './download.js';
17
17
  import { AuthenticationFailure } from './errors.js';
18
18
  import imapCommands from './imap-commands.js';
19
19
  import { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, packMessageRange, normalizePath, expandRange, getColorFlags, hasCapability, isRev2Active, logConnectionError, unrefTimer, clearTimer, parseUintValue, getStringList, getTextValues, emitSafe, buildConnectionError, guardedPromise, guardedReject, MAX_UINT32_DIGITS } from './tools.js';
20
- export { AuthenticationFailure } from './errors.js';
20
+ export { AuthenticationFailure, ImapFlowErrorCode } from './errors.js';
21
21
  const GREETING_TIMEOUT = 16 * 1000;
22
22
  const UPGRADE_TIMEOUT = 10 * 1000;
23
23
  const SOCKET_TIMEOUT = 5 * 60 * 1000;
@@ -1561,6 +1561,7 @@ export class ImapFlow extends EventEmitter {
1561
1561
  /** @internal */
1562
1562
  beginSession(onUnhandledError) {
1563
1563
  clearTimer(this.greetingTimeout);
1564
+ this.greetingReceived = true;
1564
1565
  this.untaggedHandlers.OK = null;
1565
1566
  this.untaggedHandlers.PREAUTH = null;
1566
1567
  if (this.isClosed) {
@@ -1620,6 +1621,12 @@ export class ImapFlow extends EventEmitter {
1620
1621
  this.byeReason = reason || 'Server closed connection';
1621
1622
  this.untaggedHandlers.BYE = null;
1622
1623
  this.state = this.states.LOGOUT;
1624
+ // A BYE greeting rejects the connection outright. Do not wait for the server to close
1625
+ // the socket: one that keeps it open would leave connect() pending until the greeting
1626
+ // timeout.
1627
+ if (!this.greetingReceived) {
1628
+ this.closeAfter();
1629
+ }
1623
1630
  }
1624
1631
  // Drops every capability-derived field together - the counterpart of
1625
1632
  // updateCapabilitiesFromRaw() below, which sets them together. rawCapabilities is
@@ -1852,7 +1859,7 @@ export class ImapFlow extends EventEmitter {
1852
1859
  /** @internal */
1853
1860
  autoidle() {
1854
1861
  clearTimer(this.idleStartTimer);
1855
- if (this.options.disableAutoIdle || this.state !== this.states.SELECTED) {
1862
+ if (this.options.disableAutoIdle || !this.usable || this.state !== this.states.SELECTED) {
1856
1863
  return;
1857
1864
  }
1858
1865
  if (this.connectionBusy()) {
@@ -1864,7 +1871,7 @@ export class ImapFlow extends EventEmitter {
1864
1871
  // missed clearTimeout would inject IDLE between a caller's own commands. Declining
1865
1872
  // postpones rather than cancels: whatever made the connection busy calls autoidle()
1866
1873
  // again when it finishes.
1867
- if (this.state !== this.states.SELECTED || this.connectionBusy()) {
1874
+ if (!this.usable || this.state !== this.states.SELECTED || this.connectionBusy()) {
1868
1875
  return;
1869
1876
  }
1870
1877
  this.idle().catch(err => logConnectionError(this, 'Auto-IDLE failed', err));
@@ -1883,9 +1890,17 @@ export class ImapFlow extends EventEmitter {
1883
1890
  async connect() {
1884
1891
  if (this._connectCalled) {
1885
1892
  // Prevent re-using ImapFlow instances by allowing to call connect just once.
1886
- throw new Error('Can not re-use ImapFlow instance');
1893
+ let err = new Error('Can not re-use ImapFlow instance');
1894
+ err.code = 'InstanceReused';
1895
+ throw err;
1887
1896
  }
1888
1897
  this._connectCalled = true;
1898
+ let closedError = () => this.createNoConnectionError(this.byeReason, { rejectedFrom: 'connect' });
1899
+ // close() already ran, so there is no connection to set up and nothing would ever
1900
+ // settle a connect attempt started now.
1901
+ if (this.isClosed) {
1902
+ throw closedError();
1903
+ }
1889
1904
  // One deadline for the whole attempt, started before anything is resolved or negotiated.
1890
1905
  // Proxy DNS and proxy negotiation used to run entirely outside the timer, so a stalled
1891
1906
  // proxy could hang far beyond the documented connectionTimeout.
@@ -1931,9 +1946,20 @@ export class ImapFlow extends EventEmitter {
1931
1946
  error._err = err;
1932
1947
  throw error;
1933
1948
  }
1949
+ // close() during proxy setup found no socket to destroy and no connect to reject,
1950
+ // so the tunnel that just opened is dropped here instead of being handed to a
1951
+ // closed client.
1952
+ if (this.isClosed) {
1953
+ socket.destroy();
1954
+ throw closedError();
1955
+ }
1934
1956
  }
1935
1957
  // Guarded: close() rejects a pending connect() synchronously. See guardedPromise().
1936
1958
  let connectPromise = guardedPromise((resolve, reject) => {
1959
+ // Stored before the transport is up, so a close() that lands before onConnect
1960
+ // still rejects this attempt through closeConnectSteps().
1961
+ this.initialResolve = resolve;
1962
+ this.initialReject = reject;
1937
1963
  // Whatever the proxy phase already used is gone from the budget
1938
1964
  this.connectTimeout = setTimeout(() => {
1939
1965
  let err = deadline.error();
@@ -1985,9 +2011,6 @@ export class ImapFlow extends EventEmitter {
1985
2011
  this.setSocketHandlers();
1986
2012
  this.setEventHandlers();
1987
2013
  connected.pipe(this.streamer);
1988
- // executed by initial "* OK"
1989
- this.initialResolve = resolve;
1990
- this.initialReject = reject;
1991
2014
  /* c8 ignore next 4 */ // defensive: the onConnect setup body does not throw under normal operation
1992
2015
  }
1993
2016
  catch (ex) {
@@ -2171,7 +2194,10 @@ export class ImapFlow extends EventEmitter {
2171
2194
  this._upgradeReject = null;
2172
2195
  reject(this.createNoConnectionError(false, { rejectedFrom: 'upgrade' }));
2173
2196
  }
2174
- if (typeof this.initialReject === 'function' && !this.options.verifyOnly) {
2197
+ // A verifyOnly session closes itself with LOGOUT and startSession() settles connect()
2198
+ // with the outcome, so the close is not a failure there. Before the greeting nothing
2199
+ // else can settle it (BYE greeting, FIN, close() while the transport is still coming up).
2200
+ if (typeof this.initialReject === 'function' && (!this.options.verifyOnly || !this.greetingReceived)) {
2175
2201
  clearTimer(this.greetingTimeout);
2176
2202
  let reject = this.initialReject;
2177
2203
  this.initialResolve = false;
@@ -2482,7 +2508,26 @@ export class ImapFlow extends EventEmitter {
2482
2508
  * // 125
2483
2509
  */
2484
2510
  async mailboxOpen(path, options) {
2485
- return await this.run('SELECT', path, options);
2511
+ try {
2512
+ return await this.run('SELECT', path, options);
2513
+ }
2514
+ catch (err) {
2515
+ if (err.responseStatus === 'NO') {
2516
+ // SELECT failed with NO: verify whether the mailbox exists at all by running
2517
+ // LIST. This sets mailboxMissing on the error so the caller can distinguish
2518
+ // "doesn't exist" from other failures, for getMailboxLock() callers as well.
2519
+ try {
2520
+ let folders = await this.run('LIST', '', normalizePath(this, path), { listOnly: true });
2521
+ if (!folders || !folders.length) {
2522
+ err.mailboxMissing = true;
2523
+ }
2524
+ }
2525
+ catch (E) {
2526
+ this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
2527
+ }
2528
+ }
2529
+ throw err;
2530
+ }
2486
2531
  }
2487
2532
  /**
2488
2533
  * Closes a previously opened mailbox
@@ -3184,20 +3229,7 @@ export class ImapFlow extends EventEmitter {
3184
3229
  break; // Wait for this lock to be released
3185
3230
  }
3186
3231
  catch (err) {
3187
- if (err.responseStatus === 'NO') {
3188
- // SELECT failed with NO: verify whether the mailbox exists
3189
- // at all by running LIST. This sets mailboxMissing on the error
3190
- // so the caller can distinguish "doesn't exist" from other failures.
3191
- try {
3192
- let folders = await this.run('LIST', '', path, { listOnly: true });
3193
- if (!folders || !folders.length) {
3194
- err.mailboxMissing = true;
3195
- }
3196
- }
3197
- catch (E) {
3198
- this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
3199
- }
3200
- }
3232
+ // mailboxOpen() has already marked a missing mailbox (mailboxMissing)
3201
3233
  this.log.trace({
3202
3234
  msg: 'Failed to acquire mailbox lock',
3203
3235
  path,
@@ -3288,12 +3320,14 @@ export class ImapFlow extends EventEmitter {
3288
3320
  }
3289
3321
  /** @internal */
3290
3322
  getLogger() {
3291
- let mainLogger = this.options.logger && typeof this.options.logger === 'object'
3292
- ? this.options.logger
3293
- : logger.child({
3294
- component: 'imap-connection',
3295
- cid: this.id
3296
- });
3323
+ let mainLogger = {};
3324
+ if (this.options.logger && typeof this.options.logger === 'object') {
3325
+ mainLogger = this.options.logger;
3326
+ }
3327
+ else if (this.options.logger !== false) {
3328
+ // {logger:false} never consults mainLogger, so it does not create the default logger
3329
+ mainLogger = createConnectionLogger({ cid: this.id, logRaw: this.options.logRaw });
3330
+ }
3297
3331
  let synteticLogger = {};
3298
3332
  let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
3299
3333
  for (let level of levels) {
@@ -3349,7 +3383,8 @@ export class ImapFlow extends EventEmitter {
3349
3383
  }
3350
3384
  /**
3351
3385
  * Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
3352
- * (e.g., STARTTLS) or transferring socket ownership.
3386
+ * (e.g., STARTTLS) or transferring socket ownership. Call it while the connection is not
3387
+ * idling: an IDLE in progress is not broken first, so the server still expects `DONE`.
3353
3388
  *
3354
3389
  * @returns Socket objects: `readSocket` is the read socket (inflated socket if compression is enabled, raw socket otherwise),
3355
3390
  * `writeSocket` the write socket and `socket` the raw underlying socket (same as readSocket/writeSocket when compression is disabled)
@@ -3364,6 +3399,11 @@ export class ImapFlow extends EventEmitter {
3364
3399
  // compression is active, the PassThrough writeSocket - so the connection
3365
3400
  // is fully released to the caller.
3366
3401
  this.clearSocketHandlers();
3402
+ // The socket now belongs to the caller. Marking the client unusable keeps
3403
+ // auto-IDLE (armed now, or re-armed by a lock release or a finished download)
3404
+ // and a later dispose from writing IDLE or LOGOUT onto it.
3405
+ this.usable = false;
3406
+ clearTimer(this.idleStartTimer);
3367
3407
  const readSocket = this._inflate || socket;
3368
3408
  const writeSocket = this.writeSocket || socket;
3369
3409
  // Defense-in-depth: when compression is active the raw socket is orphaned
@@ -3461,6 +3501,23 @@ export class ImapFlow extends EventEmitter {
3461
3501
  * console.log(`${entry.cid} ${entry.msg}`);
3462
3502
  * });
3463
3503
  */
3504
+ // Installed outside the class body: a computed `[Symbol.asyncDispose]` key would turn into a
3505
+ // method named "undefined" on Node.js 20.0-20.3, which predate the symbol
3506
+ if (typeof Symbol.asyncDispose === 'symbol') {
3507
+ ImapFlow.prototype[Symbol.asyncDispose] = async function () {
3508
+ if (this.usable) {
3509
+ try {
3510
+ await this.logout();
3511
+ }
3512
+ catch (err) {
3513
+ logConnectionError(this, 'LOGOUT failed while disposing', err);
3514
+ }
3515
+ }
3516
+ // logout() already closes the connection; close() is idempotent and covers a client
3517
+ // that never connected or whose logout was skipped
3518
+ this.close();
3519
+ };
3520
+ }
3464
3521
  // Both `import { ImapFlow } from 'imapflow'` and `import imapflow from 'imapflow'` work, the
3465
3522
  // latter matching the shape `require('imapflow')` has always had
3466
3523
  const imapflow = { ImapFlow, 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;
@@ -1,4 +1,34 @@
1
1
  import pino from 'pino';
2
- const logger = pino();
3
- logger.level = 'trace';
4
- export default logger;
2
+ let logger = null;
3
+ /**
4
+ * Returns the shared default logger, used when a connection is given no logger of its own.
5
+ * Created on first use, so importing the library does not set up a stdout logger nobody asked
6
+ * for. Level info: a caller that asks for raw socket data (logRaw) lowers its own child logger
7
+ * to trace.
8
+ *
9
+ * @returns The default pino logger
10
+ */
11
+ export function getDefaultLogger() {
12
+ if (!logger) {
13
+ logger = pino({ level: 'info' });
14
+ }
15
+ return logger;
16
+ }
17
+ /**
18
+ * Returns the child of the default logger a connection logs through when it was given no
19
+ * logger of its own.
20
+ *
21
+ * @param options - cid is the connection id to stamp on entries, logRaw asks for raw socket data
22
+ * @returns A child logger of the default logger
23
+ */
24
+ export function createConnectionLogger(options) {
25
+ let child = getDefaultLogger().child({
26
+ component: 'imap-connection',
27
+ cid: options.cid
28
+ });
29
+ // Raw socket data is logged at trace level, so asking for it lowers the threshold
30
+ if (options.logRaw) {
31
+ child.level = 'trace';
32
+ }
33
+ return child;
34
+ }
@@ -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/";
@@ -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.0.8";
3
+ export const version = "2.1.1";
4
4
  export const 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
@@ -1,5 +1,5 @@
1
1
  /* eslint no-control-regex:0 */
2
- import { formatDate, formatFlag, canUseFlag, toValidDate, isRev2Active } from './tools.js';
2
+ import { formatDate, formatFlag, toValidDate, isRev2Active } from './tools.js';
3
3
  /**
4
4
  * Sets a boolean flag in the IMAP search attributes.
5
5
  * Automatically handles UN- prefixing for falsy values.
@@ -88,6 +88,17 @@ let processDateField = (attributes, term, value) => {
88
88
  };
89
89
  // Pre-compiled regex for better performance
90
90
  const UNICODE_PATTERN = /[^\x00-\x7F]/;
91
+ /**
92
+ * Throws a coded search compilation error.
93
+ *
94
+ * @param code - Error code, one of the ImapFlowErrorCode values
95
+ * @param message - Error message
96
+ */
97
+ let fail = (code, message) => {
98
+ let error = new Error(message);
99
+ error.code = code;
100
+ throw error;
101
+ };
91
102
  /**
92
103
  * Checks if a string contains Unicode characters.
93
104
  * Used to determine if CHARSET UTF-8 needs to be specified.
@@ -107,7 +118,7 @@ let isUnicodeString = (str) => {
107
118
  * Compiles a JavaScript object query into IMAP search command attributes.
108
119
  * Supports standard IMAP search criteria and extensions like OBJECTID and Gmail extensions.
109
120
  *
110
- * @param connection - IMAP connection object (capabilities, enabled extensions and the current mailbox are read)
121
+ * @param connection - IMAP connection object (capabilities and enabled extensions are read)
111
122
  * @param query - Search query object
112
123
  * @returns Array of IMAP search attributes
113
124
  * @throws {Error} When required server extensions are not available
@@ -133,21 +144,29 @@ export const searchCompiler = (connection, query) => {
133
144
  const attributes = [];
134
145
  // Track if we need to specify UTF-8 charset
135
146
  let hasUnicode = false;
136
- const mailbox = connection.mailbox;
137
147
  /**
138
148
  * Recursively walks through the query object and builds IMAP attributes.
139
149
  * @param params - Query parameters to process
140
150
  */
141
151
  const walk = (params) => {
142
- // Walks a query object and wraps the resulting attributes in a
143
- // sub-array so the IMAP compiler emits parentheses around them.
144
- // Used when a single search-key is required (NOT, OR operands)
145
- // but the condition has multiple keys (RFC 3501 Section 6.4.4).
146
- let walkGrouped = (obj) => {
152
+ // Compiles one NOT or OR operand, which the caller has already put its operator in
153
+ // front of. An operand with several keys is wrapped in a sub-array so the IMAP compiler
154
+ // emits parentheses around it, as the operator takes a single search-key
155
+ // (RFC 3501 Section 6.4.4). An operand that compiles to nothing (an invalid date, an
156
+ // empty object, ...) is refused: the operator would otherwise bind to whatever
157
+ // criterion follows it and invert or widen the search.
158
+ let walkOperand = (operator, obj) => {
147
159
  let startIdx = attributes.length;
148
- walk(obj);
149
- let subAttrs = attributes.splice(startIdx);
150
- attributes.push(subAttrs);
160
+ if (obj && typeof obj === 'object') {
161
+ walk(obj);
162
+ }
163
+ if (attributes.length === startIdx) {
164
+ fail('InvalidSearchQuery', `Search operand for ${operator} does not include any usable search criteria`);
165
+ }
166
+ if (Object.keys(obj).length > 1) {
167
+ let subAttrs = attributes.splice(startIdx);
168
+ attributes.push(subAttrs);
169
+ }
151
170
  };
152
171
  Object.keys(params || {}).forEach(term => {
153
172
  switch (term.toUpperCase()) {
@@ -193,9 +212,7 @@ export const searchCompiler = (connection, query) => {
193
212
  // reject the whole search with a tagged BAD, so fail with a
194
213
  // descriptive error instead
195
214
  if (isRev2Active(connection)) {
196
- let error = new Error(`The "${term.toLowerCase()}" search key does not exist in IMAP4rev2`);
197
- error.code = 'MissingServerExtension';
198
- throw error;
215
+ fail('MissingServerExtension', `The "${term.toLowerCase()}" search key does not exist in IMAP4rev2`);
199
216
  }
200
217
  setBoolOpt(attributes, term, true);
201
218
  }
@@ -241,6 +258,11 @@ export const searchCompiler = (connection, query) => {
241
258
  // Fallback to Gmail message ID
242
259
  setOpt(attributes, 'X-GM-MSGID', params[term]);
243
260
  }
261
+ else if (params[term]) {
262
+ // Dropping the criterion would widen the search to every message
263
+ // matching the rest of the query, which a delete or move acts on
264
+ fail('MissingServerExtension', 'Server does not support OBJECTID or X-GM-EXT-1 extension required for EMAILID');
265
+ }
244
266
  break;
245
267
  // Thread ID support (OBJECTID or Gmail extension)
246
268
  case 'THREADID':
@@ -251,6 +273,11 @@ export const searchCompiler = (connection, query) => {
251
273
  // Fallback to Gmail thread ID
252
274
  setOpt(attributes, 'X-GM-THRID', params[term]);
253
275
  }
276
+ else if (params[term]) {
277
+ // Dropping the criterion would widen the search to every message
278
+ // matching the rest of the query, which a delete or move acts on
279
+ fail('MissingServerExtension', 'Server does not support OBJECTID or X-GM-EXT-1 extension required for THREADID');
280
+ }
254
281
  break;
255
282
  // Gmail raw search
256
283
  case 'GMRAW':
@@ -262,9 +289,7 @@ export const searchCompiler = (connection, query) => {
262
289
  setOpt(attributes, 'X-GM-RAW', params[term]);
263
290
  }
264
291
  else {
265
- let error = new Error('Server does not support X-GM-EXT-1 extension required for X-GM-RAW');
266
- error.code = 'MissingServerExtension';
267
- throw error;
292
+ fail('MissingServerExtension', 'Server does not support X-GM-EXT-1 extension required for X-GM-RAW');
268
293
  }
269
294
  break;
270
295
  // Gmail label search. Compiles { has, not } into an X-GM-RAW "label:"/"-label:" query
@@ -298,9 +323,7 @@ export const searchCompiler = (connection, query) => {
298
323
  break;
299
324
  }
300
325
  if (!connection.capabilities.has('X-GM-EXT-1')) {
301
- let error = new Error('Server does not support X-GM-EXT-1 extension required for label search');
302
- error.code = 'MissingServerExtension';
303
- throw error;
326
+ fail('MissingServerExtension', 'Server does not support X-GM-EXT-1 extension required for label search');
304
327
  }
305
328
  let rawQuery = rawParts.join(' ');
306
329
  if (isUnicodeString(rawQuery)) {
@@ -344,8 +367,10 @@ export const searchCompiler = (connection, query) => {
344
367
  case 'UNKEYWORD':
345
368
  {
346
369
  let flag = formatFlag(params[term]);
347
- // Only add if flag is supported or already exists in mailbox
348
- if (canUseFlag(mailbox, flag) || mailbox.flags.has(flag)) {
370
+ // Compiled even when the mailbox does not allow the keyword: the
371
+ // correct answer is then the empty set, which dropping the
372
+ // criterion would turn into every message matching the rest
373
+ if (flag) {
349
374
  setOpt(attributes, term, flag);
350
375
  }
351
376
  }
@@ -374,12 +399,7 @@ export const searchCompiler = (connection, query) => {
374
399
  case 'NOT':
375
400
  if (params[term] && typeof params[term] === 'object') {
376
401
  attributes.push({ type: 'ATOM', value: 'NOT' });
377
- if (Object.keys(params[term]).length > 1) {
378
- walkGrouped(params[term]);
379
- }
380
- else {
381
- walk(params[term]);
382
- }
402
+ walkOperand('NOT', params[term]);
383
403
  }
384
404
  break;
385
405
  // OR operator - complex logic for building OR trees
@@ -438,14 +458,7 @@ export const searchCompiler = (connection, query) => {
438
458
  entry.forEach(walkOrTree);
439
459
  return;
440
460
  }
441
- if (entry && typeof entry === 'object') {
442
- if (Object.keys(entry).length > 1) {
443
- walkGrouped(entry);
444
- }
445
- else {
446
- walk(entry);
447
- }
448
- }
461
+ walkOperand('OR', entry);
449
462
  };
450
463
  walkOrTree(genOrTree(params[term]));
451
464
  }
@@ -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/esm/tools.js CHANGED
@@ -5,10 +5,11 @@ import { compiler } from './handler/imap-handler.js';
5
5
  import { createHash } from 'node:crypto';
6
6
  import { JPDecoder } from './jp-decoder.js';
7
7
  import iconv from 'iconv-lite';
8
+ import { ImapFlowErrorCode } from './errors.js';
8
9
  export { AuthenticationFailure } from './errors.js';
9
10
  const FLAG_COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];
10
11
  // Error codes that only mean the connection is no longer usable. See logConnectionError().
11
- const CONNECTION_GONE_CODES = new Set(['NoConnection', 'EConnectionClosed', 'StateLogout']);
12
+ const CONNECTION_GONE_CODES = new Set([ImapFlowErrorCode.NoConnection, ImapFlowErrorCode.EConnectionClosed, ImapFlowErrorCode.StateLogout]);
12
13
  // Upper bound for expanding server-supplied sequence ranges (see expandRange). 2^24
13
14
  // entries in total is far beyond any legitimate mailbox while keeping the worst-case
14
15
  // expansion of a hostile range set bounded.
@@ -760,11 +761,18 @@ export async function formatMessageResponse(untagged, mailbox) {
760
761
  case 'flags':
761
762
  map.flags = new Set(getArray(attribute));
762
763
  break;
764
+ // A server with nothing to report can answer ENVELOPE NIL or BODYSTRUCTURE NIL.
765
+ // The field is left unset then: parsing NIL threw, and the whole message
766
+ // disappeared from the FETCH result.
763
767
  case 'envelope':
764
- map.envelope = parseEnvelope(attribute);
768
+ if (Array.isArray(attribute)) {
769
+ map.envelope = parseEnvelope(attribute);
770
+ }
765
771
  break;
766
772
  case 'bodystructure':
767
- map.bodyStructure = parseBodystructure(attribute);
773
+ if (Array.isArray(attribute)) {
774
+ map.bodyStructure = parseBodystructure(attribute);
775
+ }
768
776
  break;
769
777
  case 'internaldate': {
770
778
  let value = getString(attribute);
@@ -1092,7 +1100,7 @@ export function parseBodystructure(entry) {
1092
1100
  curNode.type = 'multipart/' + ((node[i++] || {}).value || '').toString().toLowerCase();
1093
1101
  // extension data (not available for BODY requests)
1094
1102
  // body parameter parenthesized list
1095
- if (i < node.length - 1) {
1103
+ if (i < node.length) {
1096
1104
  if (node[i]) {
1097
1105
  curNode.parameters = getStructuredParams(node[i]);
1098
1106
  }
@@ -1174,7 +1182,7 @@ export function parseBodystructure(entry) {
1174
1182
  }
1175
1183
  // extension data (not available for BODY requests)
1176
1184
  // md5
1177
- if (i < node.length - 1) {
1185
+ if (i < node.length) {
1178
1186
  if (node[i]) {
1179
1187
  curNode.md5 = (node[i].value || '').toString().toLowerCase();
1180
1188
  }
@@ -1184,7 +1192,7 @@ export function parseBodystructure(entry) {
1184
1192
  // the following are shared extension values (for both multipart and non-multipart parts)
1185
1193
  // not available for BODY requests
1186
1194
  // body disposition
1187
- if (i < node.length - 1) {
1195
+ if (i < node.length) {
1188
1196
  let disposition = node[i];
1189
1197
  if (Array.isArray(disposition) && disposition.length) {
1190
1198
  curNode.disposition = ((disposition[0] && disposition[0].value) || '').toString().toLowerCase();
@@ -1195,7 +1203,7 @@ export function parseBodystructure(entry) {
1195
1203
  i++;
1196
1204
  }
1197
1205
  // body language
1198
- if (i < node.length - 1) {
1206
+ if (i < node.length) {
1199
1207
  if (node[i]) {
1200
1208
  /* c8 ignore next */ // node[i] is truthy inside this guard, so the [] fallback is unreachable
1201
1209
  curNode.language = [].concat(node[i] || []).map(val => ((val && val.value) || '').toString().toLowerCase());
@@ -1205,7 +1213,7 @@ export function parseBodystructure(entry) {
1205
1213
  // body location
1206
1214
  // NB! defined as a "string list" in RFC3501 but replaced in errata document with "string"
1207
1215
  // Errata: http://www.rfc-editor.org/errata_search.php?rfc=3501
1208
- if (i < node.length - 1) {
1216
+ if (i < node.length) {
1209
1217
  if (node[i]) {
1210
1218
  curNode.location = (node[i].value || '').toString();
1211
1219
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "2.0.8",
3
+ "version": "2.1.1",
4
4
  "description": "IMAP Client for Node",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/imap-flow.js",
@@ -78,14 +78,14 @@
78
78
  "types-node-legacy": "npm:@types/node@20.0.0",
79
79
  "typescript": "6.0.3",
80
80
  "typescript-eslint": "8.70.1",
81
- "wrangler": "4.141.0"
81
+ "wrangler": "4.142.0"
82
82
  },
83
83
  "dependencies": {
84
- "@zone-eu/mailsplit": "5.4.17",
84
+ "@zone-eu/mailsplit": "5.4.18",
85
85
  "encoding-japanese": "2.4.0",
86
86
  "iconv-lite": "0.7.3",
87
87
  "libbase64": "1.3.0",
88
- "libmime": "5.4.4",
88
+ "libmime": "5.4.5",
89
89
  "libqp": "2.1.1",
90
90
  "pino": "10.3.1",
91
91
  "socks": "2.8.10"