imapflow 2.1.0 → 2.1.2
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 +14 -0
- package/README.md +2 -0
- package/dist/cjs/commands/list.js +20 -8
- package/dist/cjs/errors.d.ts +2 -0
- package/dist/cjs/errors.js +4 -1
- package/dist/cjs/handler/imap-stream.js +7 -11
- package/dist/cjs/imap-flow.js +54 -27
- 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.js +9 -2
- package/dist/esm/commands/list.js +20 -8
- package/dist/esm/errors.d.ts +2 -0
- package/dist/esm/errors.js +4 -1
- package/dist/esm/handler/imap-stream.js +7 -8
- package/dist/esm/imap-flow.js +54 -27
- 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.js +9 -2
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.2](https://github.com/postalsys/imapflow/compare/v2.1.1...v2.1.2) (2026-09-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **deps:** update libmime to 5.4.6 and mailsplit to 5.4.19 ([f6afb32](https://github.com/postalsys/imapflow/commit/f6afb32cbb1df43993a1df2cbd003e013bc10fa8))
|
|
9
|
+
|
|
10
|
+
## [2.1.1](https://github.com/postalsys/imapflow/compare/v2.1.0...v2.1.1) (2026-09-28)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* settle every pending connect on close, refuse empty search operands, keep NIL envelope rows, prefer top-level special-use folders ([27a93cc](https://github.com/postalsys/imapflow/commit/27a93ccd292fc95842bcc27cdc76e0cb0de613ba))
|
|
16
|
+
|
|
3
17
|
## [2.1.0](https://github.com/postalsys/imapflow/compare/v2.0.8...v2.1.0) (2026-09-27)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -68,6 +68,8 @@ const main = async () => {
|
|
|
68
68
|
main().catch(console.error);
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
An `ImapFlow` instance holds a single connection and cannot reconnect. Once the connection has closed, or after `connect()` has been called once, create a new instance to connect again. Calling `connect()` a second time on the same instance throws an error with the code `InstanceReused`.
|
|
72
|
+
|
|
71
73
|
See the [Quick Start guide](https://imapflow.com/docs/getting-started/quick-start) for more examples, including Gmail, Outlook, and Yahoo configuration.
|
|
72
74
|
|
|
73
75
|
## Documentation
|
|
@@ -151,10 +151,12 @@ async function list(connection, reference, mailbox, options) {
|
|
|
151
151
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
|
+
// A name sent as a literal arrives as a Buffer, so convert it once for both fields
|
|
155
|
+
let rawPath = ((untagged.attributes[2] && untagged.attributes[2].value) || '').toString();
|
|
154
156
|
let entry = {
|
|
155
157
|
// Decode from modified UTF-7 wire format and normalize the path
|
|
156
|
-
path: (0, tools_js_1.normalizePath)(connection, (0, tools_js_1.decodePath)(connection,
|
|
157
|
-
pathAsListed:
|
|
158
|
+
path: (0, tools_js_1.normalizePath)(connection, (0, tools_js_1.decodePath)(connection, rawPath)),
|
|
159
|
+
pathAsListed: rawPath,
|
|
158
160
|
flags: new Set((0, tools_js_1.getStringList)(untagged.attributes[0])),
|
|
159
161
|
delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
|
|
160
162
|
listed: true
|
|
@@ -382,9 +384,11 @@ async function list(connection, reference, mailbox, options) {
|
|
|
382
384
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
383
385
|
return;
|
|
384
386
|
}
|
|
387
|
+
// A name sent as a literal arrives as a Buffer, so convert it once for both fields
|
|
388
|
+
let rawPath = ((untagged.attributes[2] && untagged.attributes[2].value) || '').toString();
|
|
385
389
|
let entry = {
|
|
386
|
-
path: (0, tools_js_1.normalizePath)(connection, (0, tools_js_1.decodePath)(connection,
|
|
387
|
-
pathAsListed:
|
|
390
|
+
path: (0, tools_js_1.normalizePath)(connection, (0, tools_js_1.decodePath)(connection, rawPath)),
|
|
391
|
+
pathAsListed: rawPath,
|
|
388
392
|
flags: new Set((0, tools_js_1.getStringList)(untagged.attributes[0])),
|
|
389
393
|
delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
|
|
390
394
|
subscribed: true
|
|
@@ -450,15 +454,23 @@ async function list(connection, reference, mailbox, options) {
|
|
|
450
454
|
// Resolve special-use conflicts. Each type goes to one mailbox and each mailbox gets
|
|
451
455
|
// at most one type. Candidates are taken in priority order across all types (user >
|
|
452
456
|
// extension > name, then alphabetically), so a mailbox claimed by a stronger match
|
|
453
|
-
// leaves its other type to that type's next candidate instead of to nobody.
|
|
457
|
+
// leaves its other type to that type's next candidate instead of to nobody. Within a
|
|
458
|
+
// source a shallower mailbox wins before the alphabetical order is consulted, so that
|
|
459
|
+
// INBOX.Sent is preferred over INBOX.Archive.Sent.
|
|
454
460
|
let candidates = Object.entries(specialUseMatches).flatMap(([type, matches]) => matches.map(match => ({ type, ...match })));
|
|
455
461
|
candidates.sort((a, b) => {
|
|
456
462
|
let aSource = SOURCE_SORT_ORDER.indexOf(a.source);
|
|
457
463
|
let bSource = SOURCE_SORT_ORDER.indexOf(b.source);
|
|
458
|
-
if (aSource
|
|
459
|
-
return
|
|
464
|
+
if (aSource !== bSource) {
|
|
465
|
+
return aSource - bSource;
|
|
460
466
|
}
|
|
461
|
-
|
|
467
|
+
// parent is set on every listed entry before the candidates are ranked
|
|
468
|
+
let aDepth = a.entry.parent.length;
|
|
469
|
+
let bDepth = b.entry.parent.length;
|
|
470
|
+
if (aDepth !== bDepth) {
|
|
471
|
+
return aDepth - bDepth;
|
|
472
|
+
}
|
|
473
|
+
return a.entry.path.localeCompare(b.entry.path);
|
|
462
474
|
});
|
|
463
475
|
let assignedTypes = new Set();
|
|
464
476
|
for (let { type, entry, source } of candidates) {
|
package/dist/cjs/errors.d.ts
CHANGED
|
@@ -37,12 +37,14 @@ export declare const ImapFlowErrorCode: {
|
|
|
37
37
|
readonly InvalidTokenValue: "InvalidTokenValue";
|
|
38
38
|
readonly InvalidTextValue: "InvalidTextValue";
|
|
39
39
|
readonly InvalidSequenceSet: "InvalidSequenceSet";
|
|
40
|
+
readonly InvalidSearchQuery: "InvalidSearchQuery";
|
|
40
41
|
readonly DownloadOverflow: "DownloadOverflow";
|
|
41
42
|
readonly DownloadIncomplete: "DownloadIncomplete";
|
|
42
43
|
readonly ProxyError: "ProxyError";
|
|
43
44
|
readonly EPROXY: "EPROXY";
|
|
44
45
|
readonly UnsupportedProxyAddress: "UnsupportedProxyAddress";
|
|
45
46
|
readonly ERR_INVALID_URL: "ERR_INVALID_URL";
|
|
47
|
+
readonly InstanceReused: "InstanceReused";
|
|
46
48
|
};
|
|
47
49
|
/** One of the {@link ImapFlowErrorCode} values */
|
|
48
50
|
export type ImapFlowErrorCode = (typeof ImapFlowErrorCode)[keyof typeof ImapFlowErrorCode];
|
package/dist/cjs/errors.js
CHANGED
|
@@ -44,6 +44,7 @@ exports.ImapFlowErrorCode = {
|
|
|
44
44
|
InvalidTokenValue: 'InvalidTokenValue',
|
|
45
45
|
InvalidTextValue: 'InvalidTextValue',
|
|
46
46
|
InvalidSequenceSet: 'InvalidSequenceSet',
|
|
47
|
+
InvalidSearchQuery: 'InvalidSearchQuery',
|
|
47
48
|
// download()
|
|
48
49
|
DownloadOverflow: 'DownloadOverflow',
|
|
49
50
|
DownloadIncomplete: 'DownloadIncomplete',
|
|
@@ -51,7 +52,9 @@ exports.ImapFlowErrorCode = {
|
|
|
51
52
|
ProxyError: 'ProxyError',
|
|
52
53
|
EPROXY: 'EPROXY',
|
|
53
54
|
UnsupportedProxyAddress: 'UnsupportedProxyAddress',
|
|
54
|
-
ERR_INVALID_URL: 'ERR_INVALID_URL'
|
|
55
|
+
ERR_INVALID_URL: 'ERR_INVALID_URL',
|
|
56
|
+
// API misuse
|
|
57
|
+
InstanceReused: 'InstanceReused'
|
|
55
58
|
};
|
|
56
59
|
/**
|
|
57
60
|
* Error subclass thrown when IMAP authentication fails.
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.ImapStream = void 0;
|
|
7
4
|
const node_stream_1 = require("node:stream");
|
|
8
|
-
const logger_js_1 =
|
|
5
|
+
const logger_js_1 = require("../logger.js");
|
|
9
6
|
const limits_js_1 = require("./limits.js");
|
|
10
7
|
const LINE = 0x01;
|
|
11
8
|
const LITERAL = 0x02;
|
|
@@ -36,13 +33,12 @@ class ImapStream extends node_stream_1.Transform {
|
|
|
36
33
|
});
|
|
37
34
|
this.options = options || {};
|
|
38
35
|
this.cid = this.options.cid;
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
});
|
|
36
|
+
if (this.options.logger && typeof this.options.logger === 'object') {
|
|
37
|
+
this.log = this.options.logger;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
this.log = (0, logger_js_1.createConnectionLogger)({ cid: this.cid, logRaw: this.options.logRaw });
|
|
41
|
+
}
|
|
46
42
|
this.readBytesCounter = 0;
|
|
47
43
|
// Maximum length of a single line (response without a literal). Bounds the line buffer
|
|
48
44
|
// so a server that never sends a line terminator cannot exhaust memory.
|
package/dist/cjs/imap-flow.js
CHANGED
|
@@ -46,7 +46,7 @@ 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");
|
|
@@ -1931,9 +1931,17 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1931
1931
|
async connect() {
|
|
1932
1932
|
if (this._connectCalled) {
|
|
1933
1933
|
// Prevent re-using ImapFlow instances by allowing to call connect just once.
|
|
1934
|
-
|
|
1934
|
+
let err = new Error('Can not re-use ImapFlow instance');
|
|
1935
|
+
err.code = 'InstanceReused';
|
|
1936
|
+
throw err;
|
|
1935
1937
|
}
|
|
1936
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
|
+
}
|
|
1937
1945
|
// One deadline for the whole attempt, started before anything is resolved or negotiated.
|
|
1938
1946
|
// Proxy DNS and proxy negotiation used to run entirely outside the timer, so a stalled
|
|
1939
1947
|
// proxy could hang far beyond the documented connectionTimeout.
|
|
@@ -1979,9 +1987,20 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1979
1987
|
error._err = err;
|
|
1980
1988
|
throw error;
|
|
1981
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
|
+
}
|
|
1982
1997
|
}
|
|
1983
1998
|
// Guarded: close() rejects a pending connect() synchronously. See guardedPromise().
|
|
1984
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;
|
|
1985
2004
|
// Whatever the proxy phase already used is gone from the budget
|
|
1986
2005
|
this.connectTimeout = setTimeout(() => {
|
|
1987
2006
|
let err = deadline.error();
|
|
@@ -2033,9 +2052,6 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2033
2052
|
this.setSocketHandlers();
|
|
2034
2053
|
this.setEventHandlers();
|
|
2035
2054
|
connected.pipe(this.streamer);
|
|
2036
|
-
// executed by initial "* OK"
|
|
2037
|
-
this.initialResolve = resolve;
|
|
2038
|
-
this.initialReject = reject;
|
|
2039
2055
|
/* c8 ignore next 4 */ // defensive: the onConnect setup body does not throw under normal operation
|
|
2040
2056
|
}
|
|
2041
2057
|
catch (ex) {
|
|
@@ -2219,7 +2235,10 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2219
2235
|
this._upgradeReject = null;
|
|
2220
2236
|
reject(this.createNoConnectionError(false, { rejectedFrom: 'upgrade' }));
|
|
2221
2237
|
}
|
|
2222
|
-
|
|
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)) {
|
|
2223
2242
|
(0, tools_js_1.clearTimer)(this.greetingTimeout);
|
|
2224
2243
|
let reject = this.initialReject;
|
|
2225
2244
|
this.initialResolve = false;
|
|
@@ -2530,7 +2549,26 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2530
2549
|
* // 125
|
|
2531
2550
|
*/
|
|
2532
2551
|
async mailboxOpen(path, options) {
|
|
2533
|
-
|
|
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
|
+
}
|
|
2534
2572
|
}
|
|
2535
2573
|
/**
|
|
2536
2574
|
* Closes a previously opened mailbox
|
|
@@ -3232,20 +3270,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3232
3270
|
break; // Wait for this lock to be released
|
|
3233
3271
|
}
|
|
3234
3272
|
catch (err) {
|
|
3235
|
-
|
|
3236
|
-
// SELECT failed with NO: verify whether the mailbox exists
|
|
3237
|
-
// at all by running LIST. This sets mailboxMissing on the error
|
|
3238
|
-
// so the caller can distinguish "doesn't exist" from other failures.
|
|
3239
|
-
try {
|
|
3240
|
-
let folders = await this.run('LIST', '', path, { listOnly: true });
|
|
3241
|
-
if (!folders || !folders.length) {
|
|
3242
|
-
err.mailboxMissing = true;
|
|
3243
|
-
}
|
|
3244
|
-
}
|
|
3245
|
-
catch (E) {
|
|
3246
|
-
this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
|
|
3247
|
-
}
|
|
3248
|
-
}
|
|
3273
|
+
// mailboxOpen() has already marked a missing mailbox (mailboxMissing)
|
|
3249
3274
|
this.log.trace({
|
|
3250
3275
|
msg: 'Failed to acquire mailbox lock',
|
|
3251
3276
|
path,
|
|
@@ -3336,12 +3361,14 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3336
3361
|
}
|
|
3337
3362
|
/** @internal */
|
|
3338
3363
|
getLogger() {
|
|
3339
|
-
let mainLogger =
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
}
|
|
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
|
+
}
|
|
3345
3372
|
let synteticLogger = {};
|
|
3346
3373
|
let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|
3347
3374
|
for (let level of levels) {
|
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.js
CHANGED
|
@@ -816,11 +816,18 @@ async function formatMessageResponse(untagged, mailbox) {
|
|
|
816
816
|
case 'flags':
|
|
817
817
|
map.flags = new Set(getArray(attribute));
|
|
818
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.
|
|
819
822
|
case 'envelope':
|
|
820
|
-
|
|
823
|
+
if (Array.isArray(attribute)) {
|
|
824
|
+
map.envelope = parseEnvelope(attribute);
|
|
825
|
+
}
|
|
821
826
|
break;
|
|
822
827
|
case 'bodystructure':
|
|
823
|
-
|
|
828
|
+
if (Array.isArray(attribute)) {
|
|
829
|
+
map.bodyStructure = parseBodystructure(attribute);
|
|
830
|
+
}
|
|
824
831
|
break;
|
|
825
832
|
case 'internaldate': {
|
|
826
833
|
let value = getString(attribute);
|
|
@@ -148,10 +148,12 @@ export default async function list(connection, reference, mailbox, options) {
|
|
|
148
148
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
|
+
// A name sent as a literal arrives as a Buffer, so convert it once for both fields
|
|
152
|
+
let rawPath = ((untagged.attributes[2] && untagged.attributes[2].value) || '').toString();
|
|
151
153
|
let entry = {
|
|
152
154
|
// Decode from modified UTF-7 wire format and normalize the path
|
|
153
|
-
path: normalizePath(connection, decodePath(connection,
|
|
154
|
-
pathAsListed:
|
|
155
|
+
path: normalizePath(connection, decodePath(connection, rawPath)),
|
|
156
|
+
pathAsListed: rawPath,
|
|
155
157
|
flags: new Set(getStringList(untagged.attributes[0])),
|
|
156
158
|
delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
|
|
157
159
|
listed: true
|
|
@@ -379,9 +381,11 @@ export default async function list(connection, reference, mailbox, options) {
|
|
|
379
381
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
380
382
|
return;
|
|
381
383
|
}
|
|
384
|
+
// A name sent as a literal arrives as a Buffer, so convert it once for both fields
|
|
385
|
+
let rawPath = ((untagged.attributes[2] && untagged.attributes[2].value) || '').toString();
|
|
382
386
|
let entry = {
|
|
383
|
-
path: normalizePath(connection, decodePath(connection,
|
|
384
|
-
pathAsListed:
|
|
387
|
+
path: normalizePath(connection, decodePath(connection, rawPath)),
|
|
388
|
+
pathAsListed: rawPath,
|
|
385
389
|
flags: new Set(getStringList(untagged.attributes[0])),
|
|
386
390
|
delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
|
|
387
391
|
subscribed: true
|
|
@@ -447,15 +451,23 @@ export default async function list(connection, reference, mailbox, options) {
|
|
|
447
451
|
// Resolve special-use conflicts. Each type goes to one mailbox and each mailbox gets
|
|
448
452
|
// at most one type. Candidates are taken in priority order across all types (user >
|
|
449
453
|
// extension > name, then alphabetically), so a mailbox claimed by a stronger match
|
|
450
|
-
// leaves its other type to that type's next candidate instead of to nobody.
|
|
454
|
+
// leaves its other type to that type's next candidate instead of to nobody. Within a
|
|
455
|
+
// source a shallower mailbox wins before the alphabetical order is consulted, so that
|
|
456
|
+
// INBOX.Sent is preferred over INBOX.Archive.Sent.
|
|
451
457
|
let candidates = Object.entries(specialUseMatches).flatMap(([type, matches]) => matches.map(match => ({ type, ...match })));
|
|
452
458
|
candidates.sort((a, b) => {
|
|
453
459
|
let aSource = SOURCE_SORT_ORDER.indexOf(a.source);
|
|
454
460
|
let bSource = SOURCE_SORT_ORDER.indexOf(b.source);
|
|
455
|
-
if (aSource
|
|
456
|
-
return
|
|
461
|
+
if (aSource !== bSource) {
|
|
462
|
+
return aSource - bSource;
|
|
457
463
|
}
|
|
458
|
-
|
|
464
|
+
// parent is set on every listed entry before the candidates are ranked
|
|
465
|
+
let aDepth = a.entry.parent.length;
|
|
466
|
+
let bDepth = b.entry.parent.length;
|
|
467
|
+
if (aDepth !== bDepth) {
|
|
468
|
+
return aDepth - bDepth;
|
|
469
|
+
}
|
|
470
|
+
return a.entry.path.localeCompare(b.entry.path);
|
|
459
471
|
});
|
|
460
472
|
let assignedTypes = new Set();
|
|
461
473
|
for (let { type, entry, source } of candidates) {
|
package/dist/esm/errors.d.ts
CHANGED
|
@@ -37,12 +37,14 @@ export declare const ImapFlowErrorCode: {
|
|
|
37
37
|
readonly InvalidTokenValue: "InvalidTokenValue";
|
|
38
38
|
readonly InvalidTextValue: "InvalidTextValue";
|
|
39
39
|
readonly InvalidSequenceSet: "InvalidSequenceSet";
|
|
40
|
+
readonly InvalidSearchQuery: "InvalidSearchQuery";
|
|
40
41
|
readonly DownloadOverflow: "DownloadOverflow";
|
|
41
42
|
readonly DownloadIncomplete: "DownloadIncomplete";
|
|
42
43
|
readonly ProxyError: "ProxyError";
|
|
43
44
|
readonly EPROXY: "EPROXY";
|
|
44
45
|
readonly UnsupportedProxyAddress: "UnsupportedProxyAddress";
|
|
45
46
|
readonly ERR_INVALID_URL: "ERR_INVALID_URL";
|
|
47
|
+
readonly InstanceReused: "InstanceReused";
|
|
46
48
|
};
|
|
47
49
|
/** One of the {@link ImapFlowErrorCode} values */
|
|
48
50
|
export type ImapFlowErrorCode = (typeof ImapFlowErrorCode)[keyof typeof ImapFlowErrorCode];
|
package/dist/esm/errors.js
CHANGED
|
@@ -41,6 +41,7 @@ export const ImapFlowErrorCode = {
|
|
|
41
41
|
InvalidTokenValue: 'InvalidTokenValue',
|
|
42
42
|
InvalidTextValue: 'InvalidTextValue',
|
|
43
43
|
InvalidSequenceSet: 'InvalidSequenceSet',
|
|
44
|
+
InvalidSearchQuery: 'InvalidSearchQuery',
|
|
44
45
|
// download()
|
|
45
46
|
DownloadOverflow: 'DownloadOverflow',
|
|
46
47
|
DownloadIncomplete: 'DownloadIncomplete',
|
|
@@ -48,7 +49,9 @@ export const ImapFlowErrorCode = {
|
|
|
48
49
|
ProxyError: 'ProxyError',
|
|
49
50
|
EPROXY: 'EPROXY',
|
|
50
51
|
UnsupportedProxyAddress: 'UnsupportedProxyAddress',
|
|
51
|
-
ERR_INVALID_URL: 'ERR_INVALID_URL'
|
|
52
|
+
ERR_INVALID_URL: 'ERR_INVALID_URL',
|
|
53
|
+
// API misuse
|
|
54
|
+
InstanceReused: 'InstanceReused'
|
|
52
55
|
};
|
|
53
56
|
/**
|
|
54
57
|
* Error subclass thrown when IMAP authentication fails.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Transform } from 'node:stream';
|
|
2
|
-
import
|
|
2
|
+
import { createConnectionLogger } from '../logger.js';
|
|
3
3
|
import { MAX_LITERAL_SIZE, MAX_LINE_SIZE, MAX_RESPONSE_SIZE, normalizeLimit, createLiteralTooLargeError } from './limits.js';
|
|
4
4
|
const LINE = 0x01;
|
|
5
5
|
const LITERAL = 0x02;
|
|
@@ -30,13 +30,12 @@ export class ImapStream extends Transform {
|
|
|
30
30
|
});
|
|
31
31
|
this.options = options || {};
|
|
32
32
|
this.cid = this.options.cid;
|
|
33
|
-
this.
|
|
34
|
-
this.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
33
|
+
if (this.options.logger && typeof this.options.logger === 'object') {
|
|
34
|
+
this.log = this.options.logger;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
this.log = createConnectionLogger({ cid: this.cid, logRaw: this.options.logRaw });
|
|
38
|
+
}
|
|
40
39
|
this.readBytesCounter = 0;
|
|
41
40
|
// Maximum length of a single line (response without a literal). Bounds the line buffer
|
|
42
41
|
// so a server that never sends a line terminator cannot exhaust memory.
|
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';
|
|
@@ -1890,9 +1890,17 @@ export class ImapFlow extends EventEmitter {
|
|
|
1890
1890
|
async connect() {
|
|
1891
1891
|
if (this._connectCalled) {
|
|
1892
1892
|
// Prevent re-using ImapFlow instances by allowing to call connect just once.
|
|
1893
|
-
|
|
1893
|
+
let err = new Error('Can not re-use ImapFlow instance');
|
|
1894
|
+
err.code = 'InstanceReused';
|
|
1895
|
+
throw err;
|
|
1894
1896
|
}
|
|
1895
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
|
+
}
|
|
1896
1904
|
// One deadline for the whole attempt, started before anything is resolved or negotiated.
|
|
1897
1905
|
// Proxy DNS and proxy negotiation used to run entirely outside the timer, so a stalled
|
|
1898
1906
|
// proxy could hang far beyond the documented connectionTimeout.
|
|
@@ -1938,9 +1946,20 @@ export class ImapFlow extends EventEmitter {
|
|
|
1938
1946
|
error._err = err;
|
|
1939
1947
|
throw error;
|
|
1940
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
|
+
}
|
|
1941
1956
|
}
|
|
1942
1957
|
// Guarded: close() rejects a pending connect() synchronously. See guardedPromise().
|
|
1943
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;
|
|
1944
1963
|
// Whatever the proxy phase already used is gone from the budget
|
|
1945
1964
|
this.connectTimeout = setTimeout(() => {
|
|
1946
1965
|
let err = deadline.error();
|
|
@@ -1992,9 +2011,6 @@ export class ImapFlow extends EventEmitter {
|
|
|
1992
2011
|
this.setSocketHandlers();
|
|
1993
2012
|
this.setEventHandlers();
|
|
1994
2013
|
connected.pipe(this.streamer);
|
|
1995
|
-
// executed by initial "* OK"
|
|
1996
|
-
this.initialResolve = resolve;
|
|
1997
|
-
this.initialReject = reject;
|
|
1998
2014
|
/* c8 ignore next 4 */ // defensive: the onConnect setup body does not throw under normal operation
|
|
1999
2015
|
}
|
|
2000
2016
|
catch (ex) {
|
|
@@ -2178,7 +2194,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
2178
2194
|
this._upgradeReject = null;
|
|
2179
2195
|
reject(this.createNoConnectionError(false, { rejectedFrom: 'upgrade' }));
|
|
2180
2196
|
}
|
|
2181
|
-
|
|
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)) {
|
|
2182
2201
|
clearTimer(this.greetingTimeout);
|
|
2183
2202
|
let reject = this.initialReject;
|
|
2184
2203
|
this.initialResolve = false;
|
|
@@ -2489,7 +2508,26 @@ export class ImapFlow extends EventEmitter {
|
|
|
2489
2508
|
* // 125
|
|
2490
2509
|
*/
|
|
2491
2510
|
async mailboxOpen(path, options) {
|
|
2492
|
-
|
|
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
|
+
}
|
|
2493
2531
|
}
|
|
2494
2532
|
/**
|
|
2495
2533
|
* Closes a previously opened mailbox
|
|
@@ -3191,20 +3229,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
3191
3229
|
break; // Wait for this lock to be released
|
|
3192
3230
|
}
|
|
3193
3231
|
catch (err) {
|
|
3194
|
-
|
|
3195
|
-
// SELECT failed with NO: verify whether the mailbox exists
|
|
3196
|
-
// at all by running LIST. This sets mailboxMissing on the error
|
|
3197
|
-
// so the caller can distinguish "doesn't exist" from other failures.
|
|
3198
|
-
try {
|
|
3199
|
-
let folders = await this.run('LIST', '', path, { listOnly: true });
|
|
3200
|
-
if (!folders || !folders.length) {
|
|
3201
|
-
err.mailboxMissing = true;
|
|
3202
|
-
}
|
|
3203
|
-
}
|
|
3204
|
-
catch (E) {
|
|
3205
|
-
this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
|
|
3206
|
-
}
|
|
3207
|
-
}
|
|
3232
|
+
// mailboxOpen() has already marked a missing mailbox (mailboxMissing)
|
|
3208
3233
|
this.log.trace({
|
|
3209
3234
|
msg: 'Failed to acquire mailbox lock',
|
|
3210
3235
|
path,
|
|
@@ -3295,12 +3320,14 @@ export class ImapFlow extends EventEmitter {
|
|
|
3295
3320
|
}
|
|
3296
3321
|
/** @internal */
|
|
3297
3322
|
getLogger() {
|
|
3298
|
-
let mainLogger =
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
}
|
|
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
|
+
}
|
|
3304
3331
|
let synteticLogger = {};
|
|
3305
3332
|
let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|
3306
3333
|
for (let level of levels) {
|
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.js
CHANGED
|
@@ -761,11 +761,18 @@ export async function formatMessageResponse(untagged, mailbox) {
|
|
|
761
761
|
case 'flags':
|
|
762
762
|
map.flags = new Set(getArray(attribute));
|
|
763
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.
|
|
764
767
|
case 'envelope':
|
|
765
|
-
|
|
768
|
+
if (Array.isArray(attribute)) {
|
|
769
|
+
map.envelope = parseEnvelope(attribute);
|
|
770
|
+
}
|
|
766
771
|
break;
|
|
767
772
|
case 'bodystructure':
|
|
768
|
-
|
|
773
|
+
if (Array.isArray(attribute)) {
|
|
774
|
+
map.bodyStructure = parseBodystructure(attribute);
|
|
775
|
+
}
|
|
769
776
|
break;
|
|
770
777
|
case 'internaldate': {
|
|
771
778
|
let value = getString(attribute);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/imap-flow.js",
|
|
@@ -81,12 +81,12 @@
|
|
|
81
81
|
"wrangler": "4.142.0"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@zone-eu/mailsplit": "5.4.
|
|
84
|
+
"@zone-eu/mailsplit": "5.4.19",
|
|
85
85
|
"encoding-japanese": "2.4.0",
|
|
86
86
|
"iconv-lite": "0.7.3",
|
|
87
|
-
"libbase64": "1.3.
|
|
88
|
-
"libmime": "5.4.
|
|
89
|
-
"libqp": "2.1.
|
|
87
|
+
"libbase64": "1.3.1",
|
|
88
|
+
"libmime": "5.4.6",
|
|
89
|
+
"libqp": "2.1.2",
|
|
90
90
|
"pino": "10.3.1",
|
|
91
91
|
"socks": "2.8.10"
|
|
92
92
|
},
|