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.
- package/CHANGELOG.md +27 -0
- package/README.md +2 -0
- package/dist/cjs/commands/list.js +35 -19
- package/dist/cjs/commands/select.js +7 -3
- package/dist/cjs/errors.d.ts +50 -1
- package/dist/cjs/errors.js +56 -1
- package/dist/cjs/handler/imap-stream.d.ts +13 -2
- package/dist/cjs/handler/imap-stream.js +54 -37
- package/dist/cjs/handler/token-parser.js +14 -8
- package/dist/cjs/imap-flow.d.ts +14 -77
- package/dist/cjs/imap-flow.js +89 -31
- package/dist/cjs/logger.d.ts +21 -3
- package/dist/cjs/logger.js +35 -5
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/search-compiler.d.ts +1 -1
- package/dist/cjs/search-compiler.js +48 -35
- package/dist/cjs/tools.d.ts +1 -1
- package/dist/cjs/tools.js +18 -10
- package/dist/esm/commands/list.js +35 -19
- package/dist/esm/commands/select.js +7 -3
- package/dist/esm/errors.d.ts +50 -1
- package/dist/esm/errors.js +55 -0
- package/dist/esm/handler/imap-stream.d.ts +13 -2
- package/dist/esm/handler/imap-stream.js +54 -34
- package/dist/esm/handler/token-parser.js +14 -8
- package/dist/esm/imap-flow.d.ts +14 -77
- package/dist/esm/imap-flow.js +88 -31
- package/dist/esm/logger.d.ts +21 -3
- package/dist/esm/logger.js +33 -3
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/search-compiler.d.ts +1 -1
- package/dist/esm/search-compiler.js +49 -36
- package/dist/esm/tools.d.ts +1 -1
- package/dist/esm/tools.js +16 -8
- package/package.json +4 -4
package/dist/esm/imap-flow.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
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 };
|
package/dist/esm/logger.d.ts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
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;
|
package/dist/esm/logger.js
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
import pino from 'pino';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
}
|
package/dist/esm/package-info.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
143
|
-
// sub-array so the IMAP compiler
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
348
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/esm/tools.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Transform } from 'node:stream';
|
|
2
2
|
import type { ImapFlow } from './imap-flow.js';
|
|
3
|
-
import type
|
|
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([
|
|
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
|
-
|
|
768
|
+
if (Array.isArray(attribute)) {
|
|
769
|
+
map.envelope = parseEnvelope(attribute);
|
|
770
|
+
}
|
|
765
771
|
break;
|
|
766
772
|
case 'bodystructure':
|
|
767
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
81
|
+
"wrangler": "4.142.0"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@zone-eu/mailsplit": "5.4.
|
|
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.
|
|
88
|
+
"libmime": "5.4.5",
|
|
89
89
|
"libqp": "2.1.1",
|
|
90
90
|
"pino": "10.3.1",
|
|
91
91
|
"socks": "2.8.10"
|