imapflow 2.5.9 → 2.7.0
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 +25 -0
- package/README.md +10 -0
- package/dist/cjs/commands/fetch.d.ts +2 -1
- package/dist/cjs/commands/fetch.js +8 -2
- package/dist/cjs/commands/idle.js +7 -0
- package/dist/cjs/commands/select.js +17 -6
- package/dist/cjs/commands/status.js +19 -5
- package/dist/cjs/download.js +3 -3
- package/dist/cjs/errors.d.ts +1 -0
- package/dist/cjs/errors.js +3 -1
- package/dist/cjs/imap-flow.d.ts +44 -2
- package/dist/cjs/imap-flow.js +93 -14
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/types.d.ts +33 -3
- package/dist/esm/commands/fetch.d.ts +2 -1
- package/dist/esm/commands/fetch.js +8 -2
- package/dist/esm/commands/idle.js +7 -0
- package/dist/esm/commands/select.js +17 -6
- package/dist/esm/commands/status.js +19 -5
- package/dist/esm/download.js +3 -3
- package/dist/esm/errors.d.ts +1 -0
- package/dist/esm/errors.js +3 -1
- package/dist/esm/imap-flow.d.ts +44 -2
- package/dist/esm/imap-flow.js +93 -14
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/types.d.ts +33 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.7.0](https://github.com/postalsys/imapflow/compare/v2.6.0...v2.7.0) (2026-10-21)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* expose idlingAtClose so a consumer can tell a session dropped in IDLE ([eb5ac6c](https://github.com/postalsys/imapflow/commit/eb5ac6c5307a735d4f2526c8e8a266e488dbe17f))
|
|
9
|
+
|
|
10
|
+
## [2.6.0](https://github.com/postalsys/imapflow/compare/v2.5.9...v2.6.0) (2026-10-21)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* expose mailboxList, context, connectedAt and lastClientWrite on the client ([c054bd3](https://github.com/postalsys/imapflow/commit/c054bd33ab0068531c64d613718233d3a1eff9e6))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* emit the error event at most once per connection ([434e6eb](https://github.com/postalsys/imapflow/commit/434e6eb84021892703d02a63dc28da4a8bf086d6))
|
|
21
|
+
* let idle() wait for a free connection before starting IDLE ([4a3d7df](https://github.com/postalsys/imapflow/commit/4a3d7df2116321997a966e0856d7035ed2b60992))
|
|
22
|
+
* reject a mailbox lock when the processLocks pass handling it throws ([a207cd0](https://github.com/postalsys/imapflow/commit/a207cd093535025524ff6066aed092779b0ff569))
|
|
23
|
+
* reject fetch() without a selected mailbox instead of returning nothing ([78036b8](https://github.com/postalsys/imapflow/commit/78036b806b9f70e70a78fa2bc668e340e63297af))
|
|
24
|
+
* report UIDVALIDITY and HIGHESTMODSEQ changes found by the polling fallback ([0f4c306](https://github.com/postalsys/imapflow/commit/0f4c306259ffc9bb973d4cb54f790707c100cc94))
|
|
25
|
+
* require an exact LIST match before treating a failed SELECT mailbox as existing ([99a5db0](https://github.com/postalsys/imapflow/commit/99a5db09a02da721b230fd56bb7d1b240cbb2a0f))
|
|
26
|
+
* start IDLE right away again when idle() is called inside a mailbox lock ([018967d](https://github.com/postalsys/imapflow/commit/018967ded2d6e44801c4af8e211b10870e0f2f44))
|
|
27
|
+
|
|
3
28
|
## [2.5.9](https://github.com/postalsys/imapflow/compare/v2.5.8...v2.5.9) (2026-10-20)
|
|
4
29
|
|
|
5
30
|
|
package/README.md
CHANGED
|
@@ -128,6 +128,16 @@ main().catch(console.error);
|
|
|
128
128
|
|
|
129
129
|
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`.
|
|
130
130
|
|
|
131
|
+
### Connection state
|
|
132
|
+
|
|
133
|
+
Besides `mailbox`, `usable`, `authenticated` and `capabilities`, a client exposes:
|
|
134
|
+
|
|
135
|
+
- `client.context` - data of the application's own, never read by ImapFlow. It starts as the `context` option (`new ImapFlow({ ..., context: { account: 'a1' } })`) and can be replaced at any time. Its type is `unknown`, cast it to what you store there.
|
|
136
|
+
- `client.connectedAt` - when the TCP or TLS connection was established, in milliseconds since the epoch, `null` before that.
|
|
137
|
+
- `client.lastClientWrite` - when the client last wrote to the server (a command, a literal, the `DONE` that ends IDLE), in milliseconds since the epoch, `null` before the first write. Useful for staying ahead of server inactivity timers.
|
|
138
|
+
- `client.idlingAtClose` - whether the connection was in IDLE (or the polling fallback) when it closed. `idling` is already cleared when the `close` event fires, so this tells a server dropping an idle session from one dropping a session mid-command.
|
|
139
|
+
- `client.mailboxList` - with `verifyOnly` and `includeMailboxes`, the mailboxes listed before the connection logged out; `undefined` otherwise.
|
|
140
|
+
|
|
131
141
|
See the [Quick Start guide](https://imapflow.com/docs/getting-started/quick-start) for more examples, including Gmail, Outlook, and Yahoo configuration.
|
|
132
142
|
|
|
133
143
|
## Documentation
|
|
@@ -25,6 +25,7 @@ export interface FetchCommandResult {
|
|
|
25
25
|
* @param range - Message sequence number or UID range
|
|
26
26
|
* @param query - Fetch query specifying which data to retrieve (e.g., flags, envelope, bodyStructure, headers, source, bodyParts)
|
|
27
27
|
* @param options - Fetch options
|
|
28
|
-
* @returns Object with message count and list, or undefined
|
|
28
|
+
* @returns Object with message count and list, or undefined when the range is empty
|
|
29
|
+
* @throws NoMailboxSelected when not in SELECTED state (NoConnection once the connection is gone)
|
|
29
30
|
*/
|
|
30
31
|
export default function fetch(connection: ImapFlow, range: string, query: FetchQueryObject, options?: FetchCommandOptions | undefined): Promise<FetchCommandResult | undefined>;
|
|
@@ -10,11 +10,17 @@ const errors_js_1 = require("../errors.js");
|
|
|
10
10
|
* @param range - Message sequence number or UID range
|
|
11
11
|
* @param query - Fetch query specifying which data to retrieve (e.g., flags, envelope, bodyStructure, headers, source, bodyParts)
|
|
12
12
|
* @param options - Fetch options
|
|
13
|
-
* @returns Object with message count and list, or undefined
|
|
13
|
+
* @returns Object with message count and list, or undefined when the range is empty
|
|
14
|
+
* @throws NoMailboxSelected when not in SELECTED state (NoConnection once the connection is gone)
|
|
14
15
|
*/
|
|
15
16
|
async function fetch(connection, range, query, options) {
|
|
16
17
|
let mailbox = (0, tools_js_1.getSelectedMailbox)(connection);
|
|
17
|
-
if (!mailbox
|
|
18
|
+
if (!mailbox) {
|
|
19
|
+
// queued behind a CLOSE or a failed SELECT: rejecting keeps fetch() from yielding
|
|
20
|
+
// nothing as if the mailbox were empty
|
|
21
|
+
throw connection.createNoMailboxError('FETCH');
|
|
22
|
+
}
|
|
23
|
+
if (!range) {
|
|
18
24
|
// nothing to do here
|
|
19
25
|
return;
|
|
20
26
|
}
|
|
@@ -215,6 +215,7 @@ async function pollOnce(connection, session) {
|
|
|
215
215
|
// HIGHESTMODSEQ is filtered out again unless the server advertises CONDSTORE, so a
|
|
216
216
|
// CONDSTORE session keeps mailbox.highestModseq current without asking a plain
|
|
217
217
|
// server for an item it does not know.
|
|
218
|
+
let polled = connection.mailbox;
|
|
218
219
|
let status = await connection.runInternal('STATUS', path, {
|
|
219
220
|
messages: true,
|
|
220
221
|
uidNext: true,
|
|
@@ -225,6 +226,12 @@ async function pollOnce(connection, session) {
|
|
|
225
226
|
if (!status) {
|
|
226
227
|
throw (0, errors_js_1.createImapError)('STATUS poll failed', 'PollFailed');
|
|
227
228
|
}
|
|
229
|
+
// STATUS can only tell that the UIDs changed, the selected view is stale then: select
|
|
230
|
+
// the mailbox again, which reports it as closed and opened anew with the new state
|
|
231
|
+
if (polled && connection.mailbox === polled && status.uidValidity && polled.uidValidity && status.uidValidity !== polled.uidValidity) {
|
|
232
|
+
connection.log.info({ msg: 'UIDVALIDITY changed, selecting the mailbox again', path, cid: connection.id });
|
|
233
|
+
await connection.runInternal('SELECT', path, { readOnly: session.selectCommand.command === 'EXAMINE' });
|
|
234
|
+
}
|
|
228
235
|
break;
|
|
229
236
|
}
|
|
230
237
|
case 'NOOP':
|
|
@@ -214,8 +214,13 @@ async function select(connection, pathInput, options) {
|
|
|
214
214
|
let currentMailbox = connection.mailbox;
|
|
215
215
|
connection.mailbox = false;
|
|
216
216
|
// Emit mailboxClose if we're switching from a different mailbox.
|
|
217
|
-
// Re-selecting the same mailbox (e.g., for resync) does not trigger close/open
|
|
218
|
-
|
|
217
|
+
// Re-selecting the same mailbox (e.g., for resync) does not trigger close/open, unless
|
|
218
|
+
// it came back with a new UIDVALIDITY: the old view and every UID in it are gone then,
|
|
219
|
+
// which is reported the way a fresh mailboxOpen() of another mailbox would be.
|
|
220
|
+
let reopened = !currentMailbox ||
|
|
221
|
+
currentMailbox.path !== path ||
|
|
222
|
+
(!!currentMailbox.uidValidity && !!map.uidValidity && currentMailbox.uidValidity !== map.uidValidity);
|
|
223
|
+
if (currentMailbox && reopened) {
|
|
219
224
|
(0, tools_js_1.emitSafe)(connection, 'mailboxClose', currentMailbox);
|
|
220
225
|
}
|
|
221
226
|
connection.mailbox = map;
|
|
@@ -223,13 +228,19 @@ async function select(connection, pathInput, options) {
|
|
|
223
228
|
// re-issues the SELECT to detect changes on servers without IDLE support).
|
|
224
229
|
connection.currentSelectCommand = selectCommand;
|
|
225
230
|
connection.state = connection.states.SELECTED;
|
|
226
|
-
if (
|
|
231
|
+
if (reopened || !currentMailbox) {
|
|
227
232
|
(0, tools_js_1.emitSafe)(connection, 'mailboxOpen', connection.mailbox);
|
|
228
233
|
}
|
|
229
|
-
else
|
|
234
|
+
else {
|
|
230
235
|
// A re-SELECT of the open mailbox (the SELECT polling fallback) gets its EXISTS here
|
|
231
|
-
// instead of in the global handler, so it is reported the same way
|
|
232
|
-
|
|
236
|
+
// instead of in the global handler, so it is reported the same way. Flag changes are
|
|
237
|
+
// not reported by SELECT, a new HIGHESTMODSEQ is the cue to resync them.
|
|
238
|
+
if (typeof map.exists === 'number' && map.exists !== currentMailbox.exists) {
|
|
239
|
+
(0, tools_js_1.emitSafe)(connection, 'exists', { path, count: map.exists, prevCount: currentMailbox.exists });
|
|
240
|
+
}
|
|
241
|
+
if (map.highestModseq && map.highestModseq !== currentMailbox.highestModseq) {
|
|
242
|
+
(0, tools_js_1.emitSafe)(connection, 'highestModseq', { path, highestModseq: map.highestModseq, prevHighestModseq: currentMailbox.highestModseq });
|
|
243
|
+
}
|
|
233
244
|
}
|
|
234
245
|
response.next();
|
|
235
246
|
return map;
|
|
@@ -17,8 +17,13 @@ const MAILBOX_UPDATERS = {
|
|
|
17
17
|
uidNext: (value, mailbox) => {
|
|
18
18
|
mailbox.uidNext = value;
|
|
19
19
|
},
|
|
20
|
-
highestModseq: (value, mailbox) => {
|
|
21
|
-
mailbox.highestModseq
|
|
20
|
+
highestModseq: (value, mailbox, connection, path) => {
|
|
21
|
+
let prevHighestModseq = mailbox.highestModseq;
|
|
22
|
+
if (prevHighestModseq !== value) {
|
|
23
|
+
mailbox.highestModseq = value;
|
|
24
|
+
// the changes behind it are not reported by STATUS, the event is the cue to resync
|
|
25
|
+
(0, tools_js_1.emitSafe)(connection, 'highestModseq', { path, highestModseq: value, prevHighestModseq });
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
28
|
};
|
|
24
29
|
/**
|
|
@@ -68,13 +73,22 @@ async function status(connection, path, query) {
|
|
|
68
73
|
if (!list) {
|
|
69
74
|
return;
|
|
70
75
|
}
|
|
76
|
+
let updates = [];
|
|
71
77
|
(0, status_fields_js_1.parseStatusList)(list, (key, value) => {
|
|
72
78
|
map[key] = value;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
updater(value, currentMailbox, connection, path);
|
|
79
|
+
if (currentMailbox && MAILBOX_UPDATERS[key]) {
|
|
80
|
+
updates.push([key, value]);
|
|
76
81
|
}
|
|
77
82
|
});
|
|
83
|
+
// Under a new UIDVALIDITY the counts describe a mailbox the selected view no
|
|
84
|
+
// longer matches, so they are not applied to it; the STATUS poll selects the
|
|
85
|
+
// mailbox again instead, which reports it as opened anew
|
|
86
|
+
if (currentMailbox && map.uidValidity && currentMailbox.uidValidity && map.uidValidity !== currentMailbox.uidValidity) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (let [key, value] of updates) {
|
|
90
|
+
MAILBOX_UPDATERS[key](value, currentMailbox, connection, path);
|
|
91
|
+
}
|
|
78
92
|
}
|
|
79
93
|
}
|
|
80
94
|
});
|
package/dist/cjs/download.js
CHANGED
|
@@ -236,9 +236,9 @@ async function downloadMessage(client, range, part, options) {
|
|
|
236
236
|
};
|
|
237
237
|
let response = await fetchExpected(client, range, query, downloadOptions, expected);
|
|
238
238
|
if (!response) {
|
|
239
|
-
// An empty answer
|
|
240
|
-
//
|
|
241
|
-
//
|
|
239
|
+
// An empty answer from a connection that closed while the consumer was paused
|
|
240
|
+
// between chunks is not a missing message: a caller that retries would skip it
|
|
241
|
+
// (fetchOne() itself rejects with NoConnection when it finds no mailbox then)
|
|
242
242
|
if (!client.usable) {
|
|
243
243
|
throw client.createNoConnectionError(false, { rejectedFrom: 'download' });
|
|
244
244
|
}
|
package/dist/cjs/errors.d.ts
CHANGED
|
@@ -51,6 +51,7 @@ export declare const ImapFlowErrorCode: {
|
|
|
51
51
|
readonly ERR_INVALID_URL: "ERR_INVALID_URL";
|
|
52
52
|
readonly UnsupportedTransportOption: "UnsupportedTransportOption";
|
|
53
53
|
readonly InstanceReused: "InstanceReused";
|
|
54
|
+
readonly NoMailboxSelected: "NoMailboxSelected";
|
|
54
55
|
};
|
|
55
56
|
/** One of the {@link ImapFlowErrorCode} values */
|
|
56
57
|
export type ImapFlowErrorCode = (typeof ImapFlowErrorCode)[keyof typeof ImapFlowErrorCode];
|
package/dist/cjs/errors.js
CHANGED
|
@@ -64,7 +64,9 @@ exports.ImapFlowErrorCode = {
|
|
|
64
64
|
// the transport option
|
|
65
65
|
UnsupportedTransportOption: 'UnsupportedTransportOption',
|
|
66
66
|
// API misuse
|
|
67
|
-
InstanceReused: 'InstanceReused'
|
|
67
|
+
InstanceReused: 'InstanceReused',
|
|
68
|
+
// a message command (FETCH) was called while no mailbox was selected
|
|
69
|
+
NoMailboxSelected: 'NoMailboxSelected'
|
|
68
70
|
};
|
|
69
71
|
/**
|
|
70
72
|
* Error subclass thrown when IMAP authentication fails.
|
package/dist/cjs/imap-flow.d.ts
CHANGED
|
@@ -134,6 +134,29 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
134
134
|
* Is current mailbox idling (`true`) or not (`false`)
|
|
135
135
|
*/
|
|
136
136
|
idling: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Data of the application's own, kept with the client and never read by ImapFlow. Starts as
|
|
139
|
+
* the `context` option; cast it to the type the application stores there
|
|
140
|
+
*/
|
|
141
|
+
context: unknown;
|
|
142
|
+
/**
|
|
143
|
+
* When the TCP (or TLS) connection to the server was established, in milliseconds since the
|
|
144
|
+
* epoch, `null` before that. Kept after the connection closed
|
|
145
|
+
*/
|
|
146
|
+
connectedAt: number | null;
|
|
147
|
+
/**
|
|
148
|
+
* When the client last wrote to the server (a command, a literal, the DONE that ends IDLE, an
|
|
149
|
+
* authentication response), in milliseconds since the epoch, `null` before the first write.
|
|
150
|
+
* Servers drop sessions that have not sent anything for a while, the IDLE ones included
|
|
151
|
+
* (RFC 9051 allows 30 minutes), and this is what their timer measures
|
|
152
|
+
*/
|
|
153
|
+
lastClientWrite: number | null;
|
|
154
|
+
/**
|
|
155
|
+
* Whether the connection was in IDLE (or the polling fallback) when it closed, `false` while it
|
|
156
|
+
* is open. `idling` itself is already cleared by the time the 'close' event fires, so this is
|
|
157
|
+
* what tells a server dropping an idle session from one dropping a session mid-command
|
|
158
|
+
*/
|
|
159
|
+
idlingAtClose: boolean;
|
|
137
160
|
/** Whether log entries are also emitted as 'log' events, see the `emitLogs` option */
|
|
138
161
|
emitLogs: boolean;
|
|
139
162
|
/** Hash algorithm for the fallback message id, see `ImapFlowOptions.idHashAlgorithm` */
|
|
@@ -148,6 +171,16 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
148
171
|
byeReason: string | undefined;
|
|
149
172
|
/** Negotiated TLS session details, `false` for a cleartext connection */
|
|
150
173
|
tls: TlsInfo | false | undefined;
|
|
174
|
+
/**
|
|
175
|
+
* The mailboxes a `verifyOnly` connection with `includeMailboxes` listed before it logged out,
|
|
176
|
+
* `undefined` otherwise
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* let client = new ImapFlow({ ..., verifyOnly: true, includeMailboxes: true });
|
|
180
|
+
* await client.connect();
|
|
181
|
+
* console.log(client.mailboxList.map(mailbox => mailbox.path));
|
|
182
|
+
*/
|
|
183
|
+
get mailboxList(): ListResponse[] | undefined;
|
|
151
184
|
/**
|
|
152
185
|
* Creates a client for one IMAP connection. Nothing is sent before `connect()` is called
|
|
153
186
|
*/
|
|
@@ -344,6 +377,9 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
344
377
|
* otherwise IDLE is started by default on connection inactivity. NB! If `idle()` is called manually then it does not
|
|
345
378
|
* return until IDLE is finished which means you would have to call some other command out of scope.
|
|
346
379
|
*
|
|
380
|
+
* Unlike auto-IDLE, a direct call does not wait for the connection to be free: a mailbox lock
|
|
381
|
+
* holder may idle inside its own lock, and idle() can not tell it from another caller.
|
|
382
|
+
*
|
|
347
383
|
* @returns `false` if IDLE failed, `undefined` otherwise
|
|
348
384
|
*
|
|
349
385
|
* @example
|
|
@@ -505,6 +541,9 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
505
541
|
* @param query Fetch query
|
|
506
542
|
* @param options Fetch options
|
|
507
543
|
* @yields Message data object
|
|
544
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected (for example
|
|
545
|
+
* after `mailboxClose()` or a failed `mailboxOpen()`), or `NoConnection` once the connection
|
|
546
|
+
* is gone. Earlier releases yielded nothing in that case, as for an empty mailbox
|
|
508
547
|
*
|
|
509
548
|
* @example
|
|
510
549
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -526,6 +565,7 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
526
565
|
* @param query Fetch query
|
|
527
566
|
* @param options Fetch options
|
|
528
567
|
* @returns Array of Message data object
|
|
568
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch()
|
|
529
569
|
*
|
|
530
570
|
* @example
|
|
531
571
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -542,7 +582,9 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
542
582
|
* @param seq Single UID or sequence number of the message to fetch for
|
|
543
583
|
* @param query Fetch query
|
|
544
584
|
* @param options Fetch options
|
|
545
|
-
* @returns Message data object
|
|
585
|
+
* @returns Message data object, `false` when the message was not found
|
|
586
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch().
|
|
587
|
+
* Earlier releases resolved `undefined` then
|
|
546
588
|
*
|
|
547
589
|
* @example
|
|
548
590
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -550,7 +592,7 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
550
592
|
* let lastMsg = await client.fetchOne('*', {uid: true})
|
|
551
593
|
* console.log(lastMsg.uid);
|
|
552
594
|
*/
|
|
553
|
-
fetchOne(seq: SequenceString, query: FetchQueryObject, options?: FetchOptions | undefined): Promise<FetchMessageObject | false
|
|
595
|
+
fetchOne(seq: SequenceString, query: FetchQueryObject, options?: FetchOptions | undefined): Promise<FetchMessageObject | false>;
|
|
554
596
|
/**
|
|
555
597
|
* Download either full rfc822 formatted message or a specific bodystructure part as a Stream.
|
|
556
598
|
* Bodystructure parts are decoded so the resulting stream is a binary file. Text content
|
package/dist/cjs/imap-flow.js
CHANGED
|
@@ -211,6 +211,18 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
211
211
|
* Current module version as a static class property
|
|
212
212
|
*/
|
|
213
213
|
static { this.version = packageInfo.version; }
|
|
214
|
+
/**
|
|
215
|
+
* The mailboxes a `verifyOnly` connection with `includeMailboxes` listed before it logged out,
|
|
216
|
+
* `undefined` otherwise
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* let client = new ImapFlow({ ..., verifyOnly: true, includeMailboxes: true });
|
|
220
|
+
* await client.connect();
|
|
221
|
+
* console.log(client.mailboxList.map(mailbox => mailbox.path));
|
|
222
|
+
*/
|
|
223
|
+
get mailboxList() {
|
|
224
|
+
return this._mailboxList;
|
|
225
|
+
}
|
|
214
226
|
/**
|
|
215
227
|
* Creates a client for one IMAP connection. Nothing is sent before `connect()` is called
|
|
216
228
|
*/
|
|
@@ -262,6 +274,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
262
274
|
this._throttleWaits = new Set();
|
|
263
275
|
this._upgradeReject = null;
|
|
264
276
|
this.isClosed = false;
|
|
277
|
+
this._errorReported = false;
|
|
265
278
|
this.states = states;
|
|
266
279
|
this.state = this.states.NOT_AUTHENTICATED;
|
|
267
280
|
this.lockCounter = 0;
|
|
@@ -286,6 +299,10 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
286
299
|
this.mailbox = false;
|
|
287
300
|
this.currentSelectCommand = false;
|
|
288
301
|
this.idling = false;
|
|
302
|
+
this.context = this.options.context;
|
|
303
|
+
this.connectedAt = null;
|
|
304
|
+
this.lastClientWrite = null;
|
|
305
|
+
this.idlingAtClose = false;
|
|
289
306
|
this.emitLogs = !!this.options.emitLogs;
|
|
290
307
|
this.lo = 0;
|
|
291
308
|
this.untaggedHandlers = {};
|
|
@@ -331,6 +348,15 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
331
348
|
return;
|
|
332
349
|
}
|
|
333
350
|
err._connId = err._connId || this.id;
|
|
351
|
+
// One error per connection: emitError() closes the connection, and until the deferred
|
|
352
|
+
// close() runs a second socket, deflate or parser error may still arrive. Emitting it as
|
|
353
|
+
// well would reach a consumer that removed its listener after the first one, or one that
|
|
354
|
+
// only awaited connect(), as an unhandled 'error' that throws.
|
|
355
|
+
if (this._errorReported) {
|
|
356
|
+
this.log.debug({ msg: 'Suppressed error after the connection already reported one', err, cid: this.id });
|
|
357
|
+
this.closeAfter();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
334
360
|
// During a STARTTLS handshake the upgrade owns the single error path (its settle()
|
|
335
361
|
// helper). Route the error there so a streamer-originated failure is surfaced with its
|
|
336
362
|
// real code (instead of a generic ClosedAfterConnect*) and cannot hang a verifyOnly
|
|
@@ -341,6 +367,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
341
367
|
this._upgradeReject = null;
|
|
342
368
|
if (typeof reject === 'function') {
|
|
343
369
|
// settle() clears the upgrade timer and flags, and closes the connection
|
|
370
|
+
this._errorReported = true;
|
|
344
371
|
reject(err);
|
|
345
372
|
return;
|
|
346
373
|
}
|
|
@@ -355,10 +382,12 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
355
382
|
let reject = this.initialReject;
|
|
356
383
|
this.initialResolve = false;
|
|
357
384
|
this.initialReject = false;
|
|
385
|
+
this._errorReported = true;
|
|
358
386
|
this.closeAfter();
|
|
359
387
|
reject(err);
|
|
360
388
|
return;
|
|
361
389
|
}
|
|
390
|
+
this._errorReported = true;
|
|
362
391
|
this.closeAfter();
|
|
363
392
|
this.emit('error', err);
|
|
364
393
|
}
|
|
@@ -412,6 +441,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
412
441
|
else {
|
|
413
442
|
return false;
|
|
414
443
|
}
|
|
444
|
+
this.lastClientWrite = Date.now();
|
|
415
445
|
for (let data of frames) {
|
|
416
446
|
if (this.logRaw) {
|
|
417
447
|
// Client frames of an authentication exchange carry credentials: the LOGIN
|
|
@@ -1302,7 +1332,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1302
1332
|
}
|
|
1303
1333
|
});
|
|
1304
1334
|
const socket = this.socket;
|
|
1305
|
-
|
|
1335
|
+
// on(), not once(): with compression the deflate and writeSocket errors are forwarded
|
|
1336
|
+
// onto this socket, and a second one before close() must not find it without a listener
|
|
1337
|
+
socket.on('error', this._socketError);
|
|
1306
1338
|
socket.once('close', this._socketClose);
|
|
1307
1339
|
socket.once('end', this._socketEnd);
|
|
1308
1340
|
socket.on('tlsClientError', this._socketError);
|
|
@@ -2281,6 +2313,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2281
2313
|
}
|
|
2282
2314
|
try {
|
|
2283
2315
|
(0, tools_js_1.clearTimer)(this.connectTimeout);
|
|
2316
|
+
this.connectedAt = Date.now();
|
|
2284
2317
|
// ImapFlow now owns the socket; drop the proxy's early error handler
|
|
2285
2318
|
// (its "before connection setup" message no longer applies).
|
|
2286
2319
|
(0, proxy_connection_js_1.detachEarlyErrorHandler)(socket);
|
|
@@ -2406,6 +2439,15 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2406
2439
|
}
|
|
2407
2440
|
return error;
|
|
2408
2441
|
}
|
|
2442
|
+
// What a message command called without a selected mailbox rejects with: NoMailboxSelected,
|
|
2443
|
+
// or NoConnection once the connection is gone, as that is why the mailbox is missing then
|
|
2444
|
+
/** @internal */
|
|
2445
|
+
createNoMailboxError(command) {
|
|
2446
|
+
if (!this.usable) {
|
|
2447
|
+
return this.createNoConnectionError(this.byeReason, { rejectedFrom: 'noMailbox', command });
|
|
2448
|
+
}
|
|
2449
|
+
return (0, errors_js_1.createImapError)('No mailbox is selected', 'NoMailboxSelected', { command });
|
|
2450
|
+
}
|
|
2409
2451
|
/**
|
|
2410
2452
|
* Closes TCP connection without notifying the server.
|
|
2411
2453
|
*
|
|
@@ -2423,6 +2465,8 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2423
2465
|
// poll or IDLE that unwinds after this point sees that it no longer owns the flag and
|
|
2424
2466
|
// leaves it alone (see claimIdling() in commands/idle.ts).
|
|
2425
2467
|
this._idleSession = null;
|
|
2468
|
+
// a later close() finds idling already cleared and keeps what the first one saw
|
|
2469
|
+
this.idlingAtClose = this.idlingAtClose || this.idling;
|
|
2426
2470
|
this.idling = false;
|
|
2427
2471
|
this.closeConnectSteps();
|
|
2428
2472
|
if (typeof this.preCheck === 'function') {
|
|
@@ -2846,8 +2890,14 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2846
2890
|
// LIST. This sets mailboxMissing on the error so the caller can distinguish
|
|
2847
2891
|
// "doesn't exist" from other failures, for getMailboxLock() callers as well.
|
|
2848
2892
|
try {
|
|
2849
|
-
let
|
|
2850
|
-
|
|
2893
|
+
let normalized = (0, tools_js_1.normalizePath)(this, path);
|
|
2894
|
+
let folders = await this.run('LIST', '', normalized, { listOnly: true });
|
|
2895
|
+
// The path is the LIST pattern, so a % or * in a name also matches other
|
|
2896
|
+
// mailboxes, and an entry marked \NonExistent (RFC 5258) does not exist either
|
|
2897
|
+
let exists = Array.isArray(folders) &&
|
|
2898
|
+
folders.some(entry => (0, tools_js_1.comparePaths)(this, entry.path, normalized) &&
|
|
2899
|
+
![...(entry.flags || [])].some(flag => typeof flag === 'string' && flag.toUpperCase() === '\\NONEXISTENT'));
|
|
2900
|
+
if (!exists) {
|
|
2851
2901
|
err.mailboxMissing = true;
|
|
2852
2902
|
}
|
|
2853
2903
|
}
|
|
@@ -2890,6 +2940,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2890
2940
|
* otherwise IDLE is started by default on connection inactivity. NB! If `idle()` is called manually then it does not
|
|
2891
2941
|
* return until IDLE is finished which means you would have to call some other command out of scope.
|
|
2892
2942
|
*
|
|
2943
|
+
* Unlike auto-IDLE, a direct call does not wait for the connection to be free: a mailbox lock
|
|
2944
|
+
* holder may idle inside its own lock, and idle() can not tell it from another caller.
|
|
2945
|
+
*
|
|
2893
2946
|
* @returns `false` if IDLE failed, `undefined` otherwise
|
|
2894
2947
|
*
|
|
2895
2948
|
* @example
|
|
@@ -3143,6 +3196,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3143
3196
|
* @param query Fetch query
|
|
3144
3197
|
* @param options Fetch options
|
|
3145
3198
|
* @yields Message data object
|
|
3199
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected (for example
|
|
3200
|
+
* after `mailboxClose()` or a failed `mailboxOpen()`), or `NoConnection` once the connection
|
|
3201
|
+
* is gone. Earlier releases yielded nothing in that case, as for an empty mailbox
|
|
3146
3202
|
*
|
|
3147
3203
|
* @example
|
|
3148
3204
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -3156,8 +3212,8 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3156
3212
|
async *fetch(range, query, options) {
|
|
3157
3213
|
options = options || {};
|
|
3158
3214
|
if (!this.mailbox) {
|
|
3159
|
-
//
|
|
3160
|
-
|
|
3215
|
+
// an empty result would look like an empty mailbox, for example right after close()
|
|
3216
|
+
throw this.createNoMailboxError('FETCH');
|
|
3161
3217
|
}
|
|
3162
3218
|
let resolved = await this.resolveRange(range, options);
|
|
3163
3219
|
if (!resolved) {
|
|
@@ -3267,6 +3323,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3267
3323
|
* @param query Fetch query
|
|
3268
3324
|
* @param options Fetch options
|
|
3269
3325
|
* @returns Array of Message data object
|
|
3326
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch()
|
|
3270
3327
|
*
|
|
3271
3328
|
* @example
|
|
3272
3329
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -3290,7 +3347,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3290
3347
|
* @param seq Single UID or sequence number of the message to fetch for
|
|
3291
3348
|
* @param query Fetch query
|
|
3292
3349
|
* @param options Fetch options
|
|
3293
|
-
* @returns Message data object
|
|
3350
|
+
* @returns Message data object, `false` when the message was not found
|
|
3351
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch().
|
|
3352
|
+
* Earlier releases resolved `undefined` then
|
|
3294
3353
|
*
|
|
3295
3354
|
* @example
|
|
3296
3355
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -3300,8 +3359,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3300
3359
|
*/
|
|
3301
3360
|
async fetchOne(seq, query, options) {
|
|
3302
3361
|
if (!this.mailbox) {
|
|
3303
|
-
//
|
|
3304
|
-
|
|
3362
|
+
// undefined used to be returned here, indistinguishable from a missing message for a
|
|
3363
|
+
// caller that only tests the result
|
|
3364
|
+
throw this.createNoMailboxError('FETCH');
|
|
3305
3365
|
}
|
|
3306
3366
|
if (seq === '*') {
|
|
3307
3367
|
if (!this.mailbox.exists) {
|
|
@@ -3479,6 +3539,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3479
3539
|
return;
|
|
3480
3540
|
}
|
|
3481
3541
|
this.processingLock = true;
|
|
3542
|
+
// The lock taken off the queue and not settled yet. A pass that throws out of its loop
|
|
3543
|
+
// rejects it, its caller would wait forever otherwise: it is in the queue no longer.
|
|
3544
|
+
let unsettled = null;
|
|
3482
3545
|
try {
|
|
3483
3546
|
// Process all locks in queue until empty
|
|
3484
3547
|
let processedCount = 0;
|
|
@@ -3495,6 +3558,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3495
3558
|
await new Promise(resolve => setImmediate(resolve));
|
|
3496
3559
|
}
|
|
3497
3560
|
const lock = this.locks.shift();
|
|
3561
|
+
unsettled = lock;
|
|
3498
3562
|
const { resolve, reject, path, options, lockId } = lock;
|
|
3499
3563
|
// From here on the grant/reject path owns the outcome; the acquire
|
|
3500
3564
|
// timer must not race with resolution.
|
|
@@ -3560,6 +3624,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3560
3624
|
};
|
|
3561
3625
|
if (!this.usable || !this.socket || this.socket.destroyed) {
|
|
3562
3626
|
this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId, idling: this.idling });
|
|
3627
|
+
unsettled = null;
|
|
3563
3628
|
reject(this.createNoConnectionError(false, { rejectedFrom: 'mailboxLock', path }));
|
|
3564
3629
|
continue; // Process next lock in queue
|
|
3565
3630
|
}
|
|
@@ -3572,6 +3637,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3572
3637
|
this.currentLock = lock;
|
|
3573
3638
|
armHeldTimer();
|
|
3574
3639
|
this.autoidle();
|
|
3640
|
+
unsettled = null;
|
|
3575
3641
|
resolve({ path, release });
|
|
3576
3642
|
};
|
|
3577
3643
|
if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
|
|
@@ -3614,23 +3680,35 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3614
3680
|
...(options.description && { description: options.description }),
|
|
3615
3681
|
err
|
|
3616
3682
|
});
|
|
3683
|
+
unsettled = null;
|
|
3617
3684
|
reject(err);
|
|
3618
3685
|
// Continue to next lock in queue
|
|
3619
3686
|
}
|
|
3620
3687
|
}
|
|
3621
3688
|
}
|
|
3689
|
+
catch (err) {
|
|
3690
|
+
if (unsettled) {
|
|
3691
|
+
let lock = unsettled;
|
|
3692
|
+
unsettled = null;
|
|
3693
|
+
// granted but not handed out (the grant itself failed): nobody can release it
|
|
3694
|
+
if (this.currentLock === lock) {
|
|
3695
|
+
(0, tools_js_1.clearTimer)(lock.heldWarnTimer);
|
|
3696
|
+
lock.heldWarnTimer = null;
|
|
3697
|
+
this.currentLock = false;
|
|
3698
|
+
}
|
|
3699
|
+
lock.reject(err);
|
|
3700
|
+
}
|
|
3701
|
+
throw err;
|
|
3702
|
+
}
|
|
3622
3703
|
finally {
|
|
3623
3704
|
this.processingLock = false;
|
|
3624
|
-
//
|
|
3625
|
-
//
|
|
3626
|
-
// arrived before we finished). Schedule another run if needed.
|
|
3627
|
-
/* c8 ignore start */ // requires a lock to be enqueued during an in-flight processLocks pass; not reproducible deterministically
|
|
3705
|
+
// A pass that ended with locks queued and none held (it threw out of its loop) would
|
|
3706
|
+
// leave them waiting with nothing to start another pass. Schedule one.
|
|
3628
3707
|
if (this.locks.length && !this.currentLock) {
|
|
3629
3708
|
setImmediate(() => {
|
|
3630
3709
|
this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
|
|
3631
3710
|
});
|
|
3632
3711
|
}
|
|
3633
|
-
/* c8 ignore stop */
|
|
3634
3712
|
}
|
|
3635
3713
|
}
|
|
3636
3714
|
/**
|
|
@@ -3825,7 +3903,8 @@ exports.ImapFlow = ImapFlow;
|
|
|
3825
3903
|
*/
|
|
3826
3904
|
/**
|
|
3827
3905
|
* Error event. In most cases getting an error event also means that connection is closed
|
|
3828
|
-
* and pending operations should return with a failure.
|
|
3906
|
+
* and pending operations should return with a failure. Emitted at most once per connection,
|
|
3907
|
+
* later errors while the connection is closing are only logged.
|
|
3829
3908
|
*
|
|
3830
3909
|
* @event ImapFlow#error
|
|
3831
3910
|
* @example
|
package/dist/cjs/package-info.js
CHANGED