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/dist/cjs/types.d.ts
CHANGED
|
@@ -76,8 +76,13 @@ export interface ImapFlowOptions {
|
|
|
76
76
|
emitLogs?: boolean | undefined;
|
|
77
77
|
/** If true, then logs out automatically after successful authentication */
|
|
78
78
|
verifyOnly?: boolean | undefined;
|
|
79
|
-
/** If true and verifyOnly is set, lists mailboxes */
|
|
79
|
+
/** If true and verifyOnly is set, lists mailboxes. The listing is in `client.mailboxList` once `connect()` resolved */
|
|
80
80
|
includeMailboxes?: boolean | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Data of the application's own to keep with the client, such as the account the connection
|
|
83
|
+
* belongs to. ImapFlow never reads it, it is the initial value of `client.context`
|
|
84
|
+
*/
|
|
85
|
+
context?: unknown;
|
|
81
86
|
/**
|
|
82
87
|
* Proxy URL. Supports HTTP CONNECT (http:, https:) and SOCKS (socks:, socks4:, socks4a:,
|
|
83
88
|
* socks5:) proxies. IPv6 proxy endpoints are given in URL form, e.g. `socks5://[2001:db8::1]:1080`.
|
|
@@ -789,6 +794,22 @@ export interface ExistsEvent {
|
|
|
789
794
|
/** Message count before this update */
|
|
790
795
|
prevCount: number;
|
|
791
796
|
}
|
|
797
|
+
/**
|
|
798
|
+
* Payload of the `highestModseq` event: the HIGHESTMODSEQ of the selected mailbox changed (RFC
|
|
799
|
+
* 7162), so flags or other message metadata changed on the server. Emitted when a poll on a
|
|
800
|
+
* server without IDLE (`missingIdleCommand` STATUS or SELECT) or a STATUS or re-SELECT of the
|
|
801
|
+
* open mailbox reports a new value, which is when the changes themselves are not delivered as
|
|
802
|
+
* `flags` events. A consumer resynchronizes with `fetch(range, { flags: true }, { changedSince:
|
|
803
|
+
* prevHighestModseq })`.
|
|
804
|
+
*/
|
|
805
|
+
export interface HighestModseqEvent {
|
|
806
|
+
/** Mailbox path */
|
|
807
|
+
path: string;
|
|
808
|
+
/** The new HIGHESTMODSEQ value */
|
|
809
|
+
highestModseq: bigint;
|
|
810
|
+
/** The value before this update, undefined when none was known */
|
|
811
|
+
prevHighestModseq?: bigint | undefined;
|
|
812
|
+
}
|
|
792
813
|
export interface FlagsEvent {
|
|
793
814
|
/** Mailbox path */
|
|
794
815
|
path: string;
|
|
@@ -888,7 +909,10 @@ export interface TlsInfo extends CipherNameAndProtocol {
|
|
|
888
909
|
export interface ImapFlowEvents {
|
|
889
910
|
/** Connection close event */
|
|
890
911
|
close: [];
|
|
891
|
-
/**
|
|
912
|
+
/**
|
|
913
|
+
* Error event. Emitted at most once per connection, the connection closes after it. An error
|
|
914
|
+
* while `connect()` is pending rejects `connect()` instead and is not emitted
|
|
915
|
+
*/
|
|
892
916
|
error: [error: Error];
|
|
893
917
|
/** Message count in currently opened mailbox changed */
|
|
894
918
|
exists: [data: ExistsEvent];
|
|
@@ -896,7 +920,13 @@ export interface ImapFlowEvents {
|
|
|
896
920
|
expunge: [data: ExpungeEvent];
|
|
897
921
|
/** Flags were updated for a message */
|
|
898
922
|
flags: [data: FlagsEvent];
|
|
899
|
-
/**
|
|
923
|
+
/** The HIGHESTMODSEQ of the selected mailbox changed without the changes being reported, see {@link HighestModseqEvent} */
|
|
924
|
+
highestModseq: [data: HighestModseqEvent];
|
|
925
|
+
/**
|
|
926
|
+
* Mailbox was opened. Also emitted, after `mailboxClose` for the old mailbox object, when the
|
|
927
|
+
* open mailbox turned out to have a new UIDVALIDITY (a re-SELECT, or a STATUS poll on a server
|
|
928
|
+
* without IDLE, which then selects the mailbox again): every cached UID of it is invalid
|
|
929
|
+
*/
|
|
900
930
|
mailboxOpen: [mailbox: MailboxObject];
|
|
901
931
|
/** Mailbox was closed, either explicitly or because the connection went away while a mailbox was still selected */
|
|
902
932
|
mailboxClose: [mailbox: MailboxObject];
|
|
@@ -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>;
|
|
@@ -7,11 +7,17 @@ import { createImapError } from '../errors.js';
|
|
|
7
7
|
* @param range - Message sequence number or UID range
|
|
8
8
|
* @param query - Fetch query specifying which data to retrieve (e.g., flags, envelope, bodyStructure, headers, source, bodyParts)
|
|
9
9
|
* @param options - Fetch options
|
|
10
|
-
* @returns Object with message count and list, or undefined
|
|
10
|
+
* @returns Object with message count and list, or undefined when the range is empty
|
|
11
|
+
* @throws NoMailboxSelected when not in SELECTED state (NoConnection once the connection is gone)
|
|
11
12
|
*/
|
|
12
13
|
export default async function fetch(connection, range, query, options) {
|
|
13
14
|
let mailbox = getSelectedMailbox(connection);
|
|
14
|
-
if (!mailbox
|
|
15
|
+
if (!mailbox) {
|
|
16
|
+
// queued behind a CLOSE or a failed SELECT: rejecting keeps fetch() from yielding
|
|
17
|
+
// nothing as if the mailbox were empty
|
|
18
|
+
throw connection.createNoMailboxError('FETCH');
|
|
19
|
+
}
|
|
20
|
+
if (!range) {
|
|
15
21
|
// nothing to do here
|
|
16
22
|
return;
|
|
17
23
|
}
|
|
@@ -212,6 +212,7 @@ async function pollOnce(connection, session) {
|
|
|
212
212
|
// HIGHESTMODSEQ is filtered out again unless the server advertises CONDSTORE, so a
|
|
213
213
|
// CONDSTORE session keeps mailbox.highestModseq current without asking a plain
|
|
214
214
|
// server for an item it does not know.
|
|
215
|
+
let polled = connection.mailbox;
|
|
215
216
|
let status = await connection.runInternal('STATUS', path, {
|
|
216
217
|
messages: true,
|
|
217
218
|
uidNext: true,
|
|
@@ -222,6 +223,12 @@ async function pollOnce(connection, session) {
|
|
|
222
223
|
if (!status) {
|
|
223
224
|
throw createImapError('STATUS poll failed', 'PollFailed');
|
|
224
225
|
}
|
|
226
|
+
// STATUS can only tell that the UIDs changed, the selected view is stale then: select
|
|
227
|
+
// the mailbox again, which reports it as closed and opened anew with the new state
|
|
228
|
+
if (polled && connection.mailbox === polled && status.uidValidity && polled.uidValidity && status.uidValidity !== polled.uidValidity) {
|
|
229
|
+
connection.log.info({ msg: 'UIDVALIDITY changed, selecting the mailbox again', path, cid: connection.id });
|
|
230
|
+
await connection.runInternal('SELECT', path, { readOnly: session.selectCommand.command === 'EXAMINE' });
|
|
231
|
+
}
|
|
225
232
|
break;
|
|
226
233
|
}
|
|
227
234
|
case 'NOOP':
|
|
@@ -211,8 +211,13 @@ export default async function select(connection, pathInput, options) {
|
|
|
211
211
|
let currentMailbox = connection.mailbox;
|
|
212
212
|
connection.mailbox = false;
|
|
213
213
|
// Emit mailboxClose if we're switching from a different mailbox.
|
|
214
|
-
// Re-selecting the same mailbox (e.g., for resync) does not trigger close/open
|
|
215
|
-
|
|
214
|
+
// Re-selecting the same mailbox (e.g., for resync) does not trigger close/open, unless
|
|
215
|
+
// it came back with a new UIDVALIDITY: the old view and every UID in it are gone then,
|
|
216
|
+
// which is reported the way a fresh mailboxOpen() of another mailbox would be.
|
|
217
|
+
let reopened = !currentMailbox ||
|
|
218
|
+
currentMailbox.path !== path ||
|
|
219
|
+
(!!currentMailbox.uidValidity && !!map.uidValidity && currentMailbox.uidValidity !== map.uidValidity);
|
|
220
|
+
if (currentMailbox && reopened) {
|
|
216
221
|
emitSafe(connection, 'mailboxClose', currentMailbox);
|
|
217
222
|
}
|
|
218
223
|
connection.mailbox = map;
|
|
@@ -220,13 +225,19 @@ export default async function select(connection, pathInput, options) {
|
|
|
220
225
|
// re-issues the SELECT to detect changes on servers without IDLE support).
|
|
221
226
|
connection.currentSelectCommand = selectCommand;
|
|
222
227
|
connection.state = connection.states.SELECTED;
|
|
223
|
-
if (
|
|
228
|
+
if (reopened || !currentMailbox) {
|
|
224
229
|
emitSafe(connection, 'mailboxOpen', connection.mailbox);
|
|
225
230
|
}
|
|
226
|
-
else
|
|
231
|
+
else {
|
|
227
232
|
// A re-SELECT of the open mailbox (the SELECT polling fallback) gets its EXISTS here
|
|
228
|
-
// instead of in the global handler, so it is reported the same way
|
|
229
|
-
|
|
233
|
+
// instead of in the global handler, so it is reported the same way. Flag changes are
|
|
234
|
+
// not reported by SELECT, a new HIGHESTMODSEQ is the cue to resync them.
|
|
235
|
+
if (typeof map.exists === 'number' && map.exists !== currentMailbox.exists) {
|
|
236
|
+
emitSafe(connection, 'exists', { path, count: map.exists, prevCount: currentMailbox.exists });
|
|
237
|
+
}
|
|
238
|
+
if (map.highestModseq && map.highestModseq !== currentMailbox.highestModseq) {
|
|
239
|
+
emitSafe(connection, 'highestModseq', { path, highestModseq: map.highestModseq, prevHighestModseq: currentMailbox.highestModseq });
|
|
240
|
+
}
|
|
230
241
|
}
|
|
231
242
|
response.next();
|
|
232
243
|
return map;
|
|
@@ -14,8 +14,13 @@ const MAILBOX_UPDATERS = {
|
|
|
14
14
|
uidNext: (value, mailbox) => {
|
|
15
15
|
mailbox.uidNext = value;
|
|
16
16
|
},
|
|
17
|
-
highestModseq: (value, mailbox) => {
|
|
18
|
-
mailbox.highestModseq
|
|
17
|
+
highestModseq: (value, mailbox, connection, path) => {
|
|
18
|
+
let prevHighestModseq = mailbox.highestModseq;
|
|
19
|
+
if (prevHighestModseq !== value) {
|
|
20
|
+
mailbox.highestModseq = value;
|
|
21
|
+
// the changes behind it are not reported by STATUS, the event is the cue to resync
|
|
22
|
+
emitSafe(connection, 'highestModseq', { path, highestModseq: value, prevHighestModseq });
|
|
23
|
+
}
|
|
19
24
|
}
|
|
20
25
|
};
|
|
21
26
|
/**
|
|
@@ -65,13 +70,22 @@ export default async function status(connection, path, query) {
|
|
|
65
70
|
if (!list) {
|
|
66
71
|
return;
|
|
67
72
|
}
|
|
73
|
+
let updates = [];
|
|
68
74
|
parseStatusList(list, (key, value) => {
|
|
69
75
|
map[key] = value;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
updater(value, currentMailbox, connection, path);
|
|
76
|
+
if (currentMailbox && MAILBOX_UPDATERS[key]) {
|
|
77
|
+
updates.push([key, value]);
|
|
73
78
|
}
|
|
74
79
|
});
|
|
80
|
+
// Under a new UIDVALIDITY the counts describe a mailbox the selected view no
|
|
81
|
+
// longer matches, so they are not applied to it; the STATUS poll selects the
|
|
82
|
+
// mailbox again instead, which reports it as opened anew
|
|
83
|
+
if (currentMailbox && map.uidValidity && currentMailbox.uidValidity && map.uidValidity !== currentMailbox.uidValidity) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (let [key, value] of updates) {
|
|
87
|
+
MAILBOX_UPDATERS[key](value, currentMailbox, connection, path);
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
90
|
}
|
|
77
91
|
});
|
package/dist/esm/download.js
CHANGED
|
@@ -229,9 +229,9 @@ export async function downloadMessage(client, range, part, options) {
|
|
|
229
229
|
};
|
|
230
230
|
let response = await fetchExpected(client, range, query, downloadOptions, expected);
|
|
231
231
|
if (!response) {
|
|
232
|
-
// An empty answer
|
|
233
|
-
//
|
|
234
|
-
//
|
|
232
|
+
// An empty answer from a connection that closed while the consumer was paused
|
|
233
|
+
// between chunks is not a missing message: a caller that retries would skip it
|
|
234
|
+
// (fetchOne() itself rejects with NoConnection when it finds no mailbox then)
|
|
235
235
|
if (!client.usable) {
|
|
236
236
|
throw client.createNoConnectionError(false, { rejectedFrom: 'download' });
|
|
237
237
|
}
|
package/dist/esm/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/esm/errors.js
CHANGED
|
@@ -60,7 +60,9 @@ export const ImapFlowErrorCode = {
|
|
|
60
60
|
// the transport option
|
|
61
61
|
UnsupportedTransportOption: 'UnsupportedTransportOption',
|
|
62
62
|
// API misuse
|
|
63
|
-
InstanceReused: 'InstanceReused'
|
|
63
|
+
InstanceReused: 'InstanceReused',
|
|
64
|
+
// a message command (FETCH) was called while no mailbox was selected
|
|
65
|
+
NoMailboxSelected: 'NoMailboxSelected'
|
|
64
66
|
};
|
|
65
67
|
/**
|
|
66
68
|
* Error subclass thrown when IMAP authentication fails.
|
package/dist/esm/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/esm/imap-flow.js
CHANGED
|
@@ -168,6 +168,18 @@ export class ImapFlow extends EventEmitter {
|
|
|
168
168
|
* Current module version as a static class property
|
|
169
169
|
*/
|
|
170
170
|
static { this.version = packageInfo.version; }
|
|
171
|
+
/**
|
|
172
|
+
* The mailboxes a `verifyOnly` connection with `includeMailboxes` listed before it logged out,
|
|
173
|
+
* `undefined` otherwise
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* let client = new ImapFlow({ ..., verifyOnly: true, includeMailboxes: true });
|
|
177
|
+
* await client.connect();
|
|
178
|
+
* console.log(client.mailboxList.map(mailbox => mailbox.path));
|
|
179
|
+
*/
|
|
180
|
+
get mailboxList() {
|
|
181
|
+
return this._mailboxList;
|
|
182
|
+
}
|
|
171
183
|
/**
|
|
172
184
|
* Creates a client for one IMAP connection. Nothing is sent before `connect()` is called
|
|
173
185
|
*/
|
|
@@ -219,6 +231,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
219
231
|
this._throttleWaits = new Set();
|
|
220
232
|
this._upgradeReject = null;
|
|
221
233
|
this.isClosed = false;
|
|
234
|
+
this._errorReported = false;
|
|
222
235
|
this.states = states;
|
|
223
236
|
this.state = this.states.NOT_AUTHENTICATED;
|
|
224
237
|
this.lockCounter = 0;
|
|
@@ -243,6 +256,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
243
256
|
this.mailbox = false;
|
|
244
257
|
this.currentSelectCommand = false;
|
|
245
258
|
this.idling = false;
|
|
259
|
+
this.context = this.options.context;
|
|
260
|
+
this.connectedAt = null;
|
|
261
|
+
this.lastClientWrite = null;
|
|
262
|
+
this.idlingAtClose = false;
|
|
246
263
|
this.emitLogs = !!this.options.emitLogs;
|
|
247
264
|
this.lo = 0;
|
|
248
265
|
this.untaggedHandlers = {};
|
|
@@ -288,6 +305,15 @@ export class ImapFlow extends EventEmitter {
|
|
|
288
305
|
return;
|
|
289
306
|
}
|
|
290
307
|
err._connId = err._connId || this.id;
|
|
308
|
+
// One error per connection: emitError() closes the connection, and until the deferred
|
|
309
|
+
// close() runs a second socket, deflate or parser error may still arrive. Emitting it as
|
|
310
|
+
// well would reach a consumer that removed its listener after the first one, or one that
|
|
311
|
+
// only awaited connect(), as an unhandled 'error' that throws.
|
|
312
|
+
if (this._errorReported) {
|
|
313
|
+
this.log.debug({ msg: 'Suppressed error after the connection already reported one', err, cid: this.id });
|
|
314
|
+
this.closeAfter();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
291
317
|
// During a STARTTLS handshake the upgrade owns the single error path (its settle()
|
|
292
318
|
// helper). Route the error there so a streamer-originated failure is surfaced with its
|
|
293
319
|
// real code (instead of a generic ClosedAfterConnect*) and cannot hang a verifyOnly
|
|
@@ -298,6 +324,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
298
324
|
this._upgradeReject = null;
|
|
299
325
|
if (typeof reject === 'function') {
|
|
300
326
|
// settle() clears the upgrade timer and flags, and closes the connection
|
|
327
|
+
this._errorReported = true;
|
|
301
328
|
reject(err);
|
|
302
329
|
return;
|
|
303
330
|
}
|
|
@@ -312,10 +339,12 @@ export class ImapFlow extends EventEmitter {
|
|
|
312
339
|
let reject = this.initialReject;
|
|
313
340
|
this.initialResolve = false;
|
|
314
341
|
this.initialReject = false;
|
|
342
|
+
this._errorReported = true;
|
|
315
343
|
this.closeAfter();
|
|
316
344
|
reject(err);
|
|
317
345
|
return;
|
|
318
346
|
}
|
|
347
|
+
this._errorReported = true;
|
|
319
348
|
this.closeAfter();
|
|
320
349
|
this.emit('error', err);
|
|
321
350
|
}
|
|
@@ -369,6 +398,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
369
398
|
else {
|
|
370
399
|
return false;
|
|
371
400
|
}
|
|
401
|
+
this.lastClientWrite = Date.now();
|
|
372
402
|
for (let data of frames) {
|
|
373
403
|
if (this.logRaw) {
|
|
374
404
|
// Client frames of an authentication exchange carry credentials: the LOGIN
|
|
@@ -1259,7 +1289,9 @@ export class ImapFlow extends EventEmitter {
|
|
|
1259
1289
|
}
|
|
1260
1290
|
});
|
|
1261
1291
|
const socket = this.socket;
|
|
1262
|
-
|
|
1292
|
+
// on(), not once(): with compression the deflate and writeSocket errors are forwarded
|
|
1293
|
+
// onto this socket, and a second one before close() must not find it without a listener
|
|
1294
|
+
socket.on('error', this._socketError);
|
|
1263
1295
|
socket.once('close', this._socketClose);
|
|
1264
1296
|
socket.once('end', this._socketEnd);
|
|
1265
1297
|
socket.on('tlsClientError', this._socketError);
|
|
@@ -2238,6 +2270,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
2238
2270
|
}
|
|
2239
2271
|
try {
|
|
2240
2272
|
clearTimer(this.connectTimeout);
|
|
2273
|
+
this.connectedAt = Date.now();
|
|
2241
2274
|
// ImapFlow now owns the socket; drop the proxy's early error handler
|
|
2242
2275
|
// (its "before connection setup" message no longer applies).
|
|
2243
2276
|
detachEarlyErrorHandler(socket);
|
|
@@ -2363,6 +2396,15 @@ export class ImapFlow extends EventEmitter {
|
|
|
2363
2396
|
}
|
|
2364
2397
|
return error;
|
|
2365
2398
|
}
|
|
2399
|
+
// What a message command called without a selected mailbox rejects with: NoMailboxSelected,
|
|
2400
|
+
// or NoConnection once the connection is gone, as that is why the mailbox is missing then
|
|
2401
|
+
/** @internal */
|
|
2402
|
+
createNoMailboxError(command) {
|
|
2403
|
+
if (!this.usable) {
|
|
2404
|
+
return this.createNoConnectionError(this.byeReason, { rejectedFrom: 'noMailbox', command });
|
|
2405
|
+
}
|
|
2406
|
+
return createImapError('No mailbox is selected', 'NoMailboxSelected', { command });
|
|
2407
|
+
}
|
|
2366
2408
|
/**
|
|
2367
2409
|
* Closes TCP connection without notifying the server.
|
|
2368
2410
|
*
|
|
@@ -2380,6 +2422,8 @@ export class ImapFlow extends EventEmitter {
|
|
|
2380
2422
|
// poll or IDLE that unwinds after this point sees that it no longer owns the flag and
|
|
2381
2423
|
// leaves it alone (see claimIdling() in commands/idle.ts).
|
|
2382
2424
|
this._idleSession = null;
|
|
2425
|
+
// a later close() finds idling already cleared and keeps what the first one saw
|
|
2426
|
+
this.idlingAtClose = this.idlingAtClose || this.idling;
|
|
2383
2427
|
this.idling = false;
|
|
2384
2428
|
this.closeConnectSteps();
|
|
2385
2429
|
if (typeof this.preCheck === 'function') {
|
|
@@ -2803,8 +2847,14 @@ export class ImapFlow extends EventEmitter {
|
|
|
2803
2847
|
// LIST. This sets mailboxMissing on the error so the caller can distinguish
|
|
2804
2848
|
// "doesn't exist" from other failures, for getMailboxLock() callers as well.
|
|
2805
2849
|
try {
|
|
2806
|
-
let
|
|
2807
|
-
|
|
2850
|
+
let normalized = normalizePath(this, path);
|
|
2851
|
+
let folders = await this.run('LIST', '', normalized, { listOnly: true });
|
|
2852
|
+
// The path is the LIST pattern, so a % or * in a name also matches other
|
|
2853
|
+
// mailboxes, and an entry marked \NonExistent (RFC 5258) does not exist either
|
|
2854
|
+
let exists = Array.isArray(folders) &&
|
|
2855
|
+
folders.some(entry => comparePaths(this, entry.path, normalized) &&
|
|
2856
|
+
![...(entry.flags || [])].some(flag => typeof flag === 'string' && flag.toUpperCase() === '\\NONEXISTENT'));
|
|
2857
|
+
if (!exists) {
|
|
2808
2858
|
err.mailboxMissing = true;
|
|
2809
2859
|
}
|
|
2810
2860
|
}
|
|
@@ -2847,6 +2897,9 @@ export class ImapFlow extends EventEmitter {
|
|
|
2847
2897
|
* otherwise IDLE is started by default on connection inactivity. NB! If `idle()` is called manually then it does not
|
|
2848
2898
|
* return until IDLE is finished which means you would have to call some other command out of scope.
|
|
2849
2899
|
*
|
|
2900
|
+
* Unlike auto-IDLE, a direct call does not wait for the connection to be free: a mailbox lock
|
|
2901
|
+
* holder may idle inside its own lock, and idle() can not tell it from another caller.
|
|
2902
|
+
*
|
|
2850
2903
|
* @returns `false` if IDLE failed, `undefined` otherwise
|
|
2851
2904
|
*
|
|
2852
2905
|
* @example
|
|
@@ -3100,6 +3153,9 @@ export class ImapFlow extends EventEmitter {
|
|
|
3100
3153
|
* @param query Fetch query
|
|
3101
3154
|
* @param options Fetch options
|
|
3102
3155
|
* @yields Message data object
|
|
3156
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected (for example
|
|
3157
|
+
* after `mailboxClose()` or a failed `mailboxOpen()`), or `NoConnection` once the connection
|
|
3158
|
+
* is gone. Earlier releases yielded nothing in that case, as for an empty mailbox
|
|
3103
3159
|
*
|
|
3104
3160
|
* @example
|
|
3105
3161
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -3113,8 +3169,8 @@ export class ImapFlow extends EventEmitter {
|
|
|
3113
3169
|
async *fetch(range, query, options) {
|
|
3114
3170
|
options = options || {};
|
|
3115
3171
|
if (!this.mailbox) {
|
|
3116
|
-
//
|
|
3117
|
-
|
|
3172
|
+
// an empty result would look like an empty mailbox, for example right after close()
|
|
3173
|
+
throw this.createNoMailboxError('FETCH');
|
|
3118
3174
|
}
|
|
3119
3175
|
let resolved = await this.resolveRange(range, options);
|
|
3120
3176
|
if (!resolved) {
|
|
@@ -3224,6 +3280,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
3224
3280
|
* @param query Fetch query
|
|
3225
3281
|
* @param options Fetch options
|
|
3226
3282
|
* @returns Array of Message data object
|
|
3283
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch()
|
|
3227
3284
|
*
|
|
3228
3285
|
* @example
|
|
3229
3286
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -3247,7 +3304,9 @@ export class ImapFlow extends EventEmitter {
|
|
|
3247
3304
|
* @param seq Single UID or sequence number of the message to fetch for
|
|
3248
3305
|
* @param query Fetch query
|
|
3249
3306
|
* @param options Fetch options
|
|
3250
|
-
* @returns Message data object
|
|
3307
|
+
* @returns Message data object, `false` when the message was not found
|
|
3308
|
+
* @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch().
|
|
3309
|
+
* Earlier releases resolved `undefined` then
|
|
3251
3310
|
*
|
|
3252
3311
|
* @example
|
|
3253
3312
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -3257,8 +3316,9 @@ export class ImapFlow extends EventEmitter {
|
|
|
3257
3316
|
*/
|
|
3258
3317
|
async fetchOne(seq, query, options) {
|
|
3259
3318
|
if (!this.mailbox) {
|
|
3260
|
-
//
|
|
3261
|
-
|
|
3319
|
+
// undefined used to be returned here, indistinguishable from a missing message for a
|
|
3320
|
+
// caller that only tests the result
|
|
3321
|
+
throw this.createNoMailboxError('FETCH');
|
|
3262
3322
|
}
|
|
3263
3323
|
if (seq === '*') {
|
|
3264
3324
|
if (!this.mailbox.exists) {
|
|
@@ -3436,6 +3496,9 @@ export class ImapFlow extends EventEmitter {
|
|
|
3436
3496
|
return;
|
|
3437
3497
|
}
|
|
3438
3498
|
this.processingLock = true;
|
|
3499
|
+
// The lock taken off the queue and not settled yet. A pass that throws out of its loop
|
|
3500
|
+
// rejects it, its caller would wait forever otherwise: it is in the queue no longer.
|
|
3501
|
+
let unsettled = null;
|
|
3439
3502
|
try {
|
|
3440
3503
|
// Process all locks in queue until empty
|
|
3441
3504
|
let processedCount = 0;
|
|
@@ -3452,6 +3515,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
3452
3515
|
await new Promise(resolve => setImmediate(resolve));
|
|
3453
3516
|
}
|
|
3454
3517
|
const lock = this.locks.shift();
|
|
3518
|
+
unsettled = lock;
|
|
3455
3519
|
const { resolve, reject, path, options, lockId } = lock;
|
|
3456
3520
|
// From here on the grant/reject path owns the outcome; the acquire
|
|
3457
3521
|
// timer must not race with resolution.
|
|
@@ -3517,6 +3581,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
3517
3581
|
};
|
|
3518
3582
|
if (!this.usable || !this.socket || this.socket.destroyed) {
|
|
3519
3583
|
this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId, idling: this.idling });
|
|
3584
|
+
unsettled = null;
|
|
3520
3585
|
reject(this.createNoConnectionError(false, { rejectedFrom: 'mailboxLock', path }));
|
|
3521
3586
|
continue; // Process next lock in queue
|
|
3522
3587
|
}
|
|
@@ -3529,6 +3594,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
3529
3594
|
this.currentLock = lock;
|
|
3530
3595
|
armHeldTimer();
|
|
3531
3596
|
this.autoidle();
|
|
3597
|
+
unsettled = null;
|
|
3532
3598
|
resolve({ path, release });
|
|
3533
3599
|
};
|
|
3534
3600
|
if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
|
|
@@ -3571,23 +3637,35 @@ export class ImapFlow extends EventEmitter {
|
|
|
3571
3637
|
...(options.description && { description: options.description }),
|
|
3572
3638
|
err
|
|
3573
3639
|
});
|
|
3640
|
+
unsettled = null;
|
|
3574
3641
|
reject(err);
|
|
3575
3642
|
// Continue to next lock in queue
|
|
3576
3643
|
}
|
|
3577
3644
|
}
|
|
3578
3645
|
}
|
|
3646
|
+
catch (err) {
|
|
3647
|
+
if (unsettled) {
|
|
3648
|
+
let lock = unsettled;
|
|
3649
|
+
unsettled = null;
|
|
3650
|
+
// granted but not handed out (the grant itself failed): nobody can release it
|
|
3651
|
+
if (this.currentLock === lock) {
|
|
3652
|
+
clearTimer(lock.heldWarnTimer);
|
|
3653
|
+
lock.heldWarnTimer = null;
|
|
3654
|
+
this.currentLock = false;
|
|
3655
|
+
}
|
|
3656
|
+
lock.reject(err);
|
|
3657
|
+
}
|
|
3658
|
+
throw err;
|
|
3659
|
+
}
|
|
3579
3660
|
finally {
|
|
3580
3661
|
this.processingLock = false;
|
|
3581
|
-
//
|
|
3582
|
-
//
|
|
3583
|
-
// arrived before we finished). Schedule another run if needed.
|
|
3584
|
-
/* c8 ignore start */ // requires a lock to be enqueued during an in-flight processLocks pass; not reproducible deterministically
|
|
3662
|
+
// A pass that ended with locks queued and none held (it threw out of its loop) would
|
|
3663
|
+
// leave them waiting with nothing to start another pass. Schedule one.
|
|
3585
3664
|
if (this.locks.length && !this.currentLock) {
|
|
3586
3665
|
setImmediate(() => {
|
|
3587
3666
|
this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
|
|
3588
3667
|
});
|
|
3589
3668
|
}
|
|
3590
|
-
/* c8 ignore stop */
|
|
3591
3669
|
}
|
|
3592
3670
|
}
|
|
3593
3671
|
/**
|
|
@@ -3781,7 +3859,8 @@ export class ImapFlow extends EventEmitter {
|
|
|
3781
3859
|
*/
|
|
3782
3860
|
/**
|
|
3783
3861
|
* Error event. In most cases getting an error event also means that connection is closed
|
|
3784
|
-
* and pending operations should return with a failure.
|
|
3862
|
+
* and pending operations should return with a failure. Emitted at most once per connection,
|
|
3863
|
+
* later errors while the connection is closing are only logged.
|
|
3785
3864
|
*
|
|
3786
3865
|
* @event ImapFlow#error
|
|
3787
3866
|
* @example
|
package/dist/esm/package-info.js
CHANGED