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/cjs/imap-flow.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
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 };
|
package/dist/cjs/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/cjs/logger.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+
}
|
package/dist/cjs/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
|
|
@@ -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
|
|
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
|
-
//
|
|
146
|
-
// sub-array so the IMAP compiler
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/cjs/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/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
|
-
|
|
62
|
-
|
|
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([
|
|
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
|
-
|
|
823
|
+
if (Array.isArray(attribute)) {
|
|
824
|
+
map.envelope = parseEnvelope(attribute);
|
|
825
|
+
}
|
|
820
826
|
break;
|
|
821
827
|
case 'bodystructure':
|
|
822
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1271
|
+
if (i < node.length) {
|
|
1264
1272
|
if (node[i]) {
|
|
1265
1273
|
curNode.location = (node[i].value || '').toString();
|
|
1266
1274
|
}
|