imapflow 2.2.6 → 2.2.8
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 +24 -0
- package/dist/cjs/commands/append.js +10 -3
- package/dist/cjs/commands/authenticate.js +42 -22
- package/dist/cjs/commands/close.d.ts +12 -1
- package/dist/cjs/commands/close.js +4 -2
- package/dist/cjs/commands/delete.js +2 -1
- package/dist/cjs/commands/enable.js +6 -0
- package/dist/cjs/commands/esearch-parser.js +8 -2
- package/dist/cjs/commands/fetch.js +57 -14
- package/dist/cjs/commands/id.js +8 -1
- package/dist/cjs/commands/idle.js +15 -5
- package/dist/cjs/commands/list.js +10 -1
- package/dist/cjs/commands/login.js +5 -1
- package/dist/cjs/commands/logout.js +7 -0
- package/dist/cjs/commands/namespace.js +7 -3
- package/dist/cjs/commands/quota.js +3 -1
- package/dist/cjs/commands/rename.js +2 -1
- package/dist/cjs/commands/select.js +5 -0
- package/dist/cjs/commands/status.js +6 -1
- package/dist/cjs/commands/store.d.ts +1 -1
- package/dist/cjs/commands/store.js +8 -8
- package/dist/cjs/download.js +220 -95
- package/dist/cjs/handler/imap-compiler.js +19 -10
- package/dist/cjs/handler/limits.d.ts +11 -0
- package/dist/cjs/handler/limits.js +16 -1
- package/dist/cjs/handler/parser-instance.d.ts +10 -0
- package/dist/cjs/handler/parser-instance.js +25 -10
- package/dist/cjs/handler/token-parser.js +36 -28
- package/dist/cjs/imap-flow.d.ts +2 -2
- package/dist/cjs/imap-flow.js +256 -95
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/proxy-connection.js +7 -7
- package/dist/cjs/search-compiler.js +33 -13
- package/dist/cjs/special-use.js +10 -5
- package/dist/cjs/tools.d.ts +14 -4
- package/dist/cjs/tools.js +69 -9
- package/dist/cjs/types.d.ts +19 -4
- package/dist/esm/commands/append.js +11 -4
- package/dist/esm/commands/authenticate.js +43 -23
- package/dist/esm/commands/close.d.ts +12 -1
- package/dist/esm/commands/close.js +5 -3
- package/dist/esm/commands/delete.js +2 -1
- package/dist/esm/commands/enable.js +6 -0
- package/dist/esm/commands/esearch-parser.js +8 -2
- package/dist/esm/commands/fetch.js +57 -14
- package/dist/esm/commands/id.js +9 -2
- package/dist/esm/commands/idle.js +16 -6
- package/dist/esm/commands/list.js +10 -1
- package/dist/esm/commands/login.js +6 -2
- package/dist/esm/commands/logout.js +7 -0
- package/dist/esm/commands/namespace.js +7 -3
- package/dist/esm/commands/quota.js +3 -1
- package/dist/esm/commands/rename.js +2 -1
- package/dist/esm/commands/select.js +5 -0
- package/dist/esm/commands/status.js +7 -2
- package/dist/esm/commands/store.d.ts +1 -1
- package/dist/esm/commands/store.js +9 -9
- package/dist/esm/download.js +220 -95
- package/dist/esm/handler/imap-compiler.js +19 -10
- package/dist/esm/handler/limits.d.ts +11 -0
- package/dist/esm/handler/limits.js +14 -0
- package/dist/esm/handler/parser-instance.d.ts +10 -0
- package/dist/esm/handler/parser-instance.js +25 -10
- package/dist/esm/handler/token-parser.js +37 -29
- package/dist/esm/imap-flow.d.ts +2 -2
- package/dist/esm/imap-flow.js +256 -95
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/proxy-connection.js +7 -7
- package/dist/esm/search-compiler.js +33 -13
- package/dist/esm/special-use.js +10 -5
- package/dist/esm/tools.d.ts +14 -4
- package/dist/esm/tools.js +68 -9
- package/dist/esm/types.d.ts +19 -4
- package/package.json +5 -3
|
@@ -17,7 +17,8 @@ export default async function deleteMailbox(connection, path) {
|
|
|
17
17
|
// IMAP servers reject DELETE on the currently selected mailbox (RFC 3501 6.3.4).
|
|
18
18
|
let selected = getSelectedMailbox(connection);
|
|
19
19
|
if (selected && selected.path === path) {
|
|
20
|
-
|
|
20
|
+
// UNSELECT where possible, CLOSE would expunge messages flagged \Deleted
|
|
21
|
+
await connection.run('CLOSE', { unselect: true });
|
|
21
22
|
}
|
|
22
23
|
let response;
|
|
23
24
|
try {
|
|
@@ -46,6 +46,12 @@ export default async function enable(connection, extensionList) {
|
|
|
46
46
|
// extensions enabled by this command (RFC 5161), so a replace would drop
|
|
47
47
|
// grants from an earlier ENABLE call
|
|
48
48
|
connection.enabled = new Set([...connection.enabled, ...enabled]);
|
|
49
|
+
if (connection.enabled.has('QRESYNC')) {
|
|
50
|
+
// ENABLE QRESYNC is a CONDSTORE enabling command (RFC 7162 3.2.3), whether or not the
|
|
51
|
+
// server lists CONDSTORE in its ENABLED answer. Apache James advertises only QRESYNC
|
|
52
|
+
// and leaves a lone ENABLE CONDSTORE unanswered, which the RFC allows.
|
|
53
|
+
connection.enabled.add('CONDSTORE');
|
|
54
|
+
}
|
|
49
55
|
response.next();
|
|
50
56
|
return connection.enabled;
|
|
51
57
|
}
|
|
@@ -69,9 +69,15 @@ export function parseEsearchResponse(attrs) {
|
|
|
69
69
|
const items = Array.isArray(listToken) ? listToken : null;
|
|
70
70
|
if (!items || items.length < 2)
|
|
71
71
|
break;
|
|
72
|
+
const range = items[0]?.value;
|
|
73
|
+
if (typeof range !== 'string')
|
|
74
|
+
break;
|
|
75
|
+
// RFC 9394 partial-results is a sequence-set or NIL, the latter when the requested
|
|
76
|
+
// range lies past the end of the results. NIL is reported as an empty set.
|
|
77
|
+
const messages = items[1]?.value;
|
|
72
78
|
result.partial = {
|
|
73
|
-
range
|
|
74
|
-
messages:
|
|
79
|
+
range,
|
|
80
|
+
messages: typeof messages === 'string' ? messages : ''
|
|
75
81
|
};
|
|
76
82
|
break;
|
|
77
83
|
}
|
|
@@ -25,11 +25,20 @@ export default async function fetch(connection, range, query, options) {
|
|
|
25
25
|
// Every pass returns or throws: the last throttled attempt throws instead of retrying.
|
|
26
26
|
const maxRetries = 4;
|
|
27
27
|
const baseDelay = 1000; // Start with 1 second delay
|
|
28
|
+
// The highest UID (sequence number for a plain FETCH) handed to the streaming consumer. A
|
|
29
|
+
// retried FETCH answers with every message again, so a retry skips up to it: servers answer
|
|
30
|
+
// in ascending order, which keeps this to one comparison per row rather than a set of every
|
|
31
|
+
// row delivered. The consumer was otherwise given the rows before the throttle twice.
|
|
32
|
+
let maxDelivered = 0;
|
|
28
33
|
for (let retryCount = 0;; retryCount++) {
|
|
29
34
|
let messages = {
|
|
30
35
|
count: 0,
|
|
31
36
|
list: []
|
|
32
37
|
};
|
|
38
|
+
// The first error the onUntaggedFetch consumer reported through next(err). Errors thrown
|
|
39
|
+
// by untagged handlers are only logged by the connection, so it is kept here and fails
|
|
40
|
+
// the command once the FETCH completes; later messages are no longer handed to the consumer.
|
|
41
|
+
let consumerError = null;
|
|
33
42
|
let response;
|
|
34
43
|
try {
|
|
35
44
|
/* c8 ignore next */ // range is guaranteed truthy by the early-return guard above, so the '*' fallback is unreachable
|
|
@@ -53,13 +62,30 @@ export default async function fetch(connection, range, query, options) {
|
|
|
53
62
|
};
|
|
54
63
|
queryStructure.push(bodyPeek);
|
|
55
64
|
};
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
// The ALL, FAST and FULL macros may only be sent on their own, never in a list with other
|
|
66
|
+
// items (RFC 3501 section 9), and UID is always in the list, so they are expanded into
|
|
67
|
+
// the items they stand for. FULL is expanded to BODYSTRUCTURE rather than the
|
|
68
|
+
// non-extensible BODY, as documented for the full option.
|
|
69
|
+
let full = !!query.full;
|
|
70
|
+
let all = !!query.all || full;
|
|
71
|
+
let fast = !!query.fast || all;
|
|
72
|
+
let items = {
|
|
73
|
+
flags: query.flags || fast,
|
|
74
|
+
internalDate: query.internalDate || fast,
|
|
75
|
+
size: query.size || fast,
|
|
76
|
+
envelope: query.envelope || all,
|
|
77
|
+
bodyStructure: query.bodyStructure || full
|
|
78
|
+
};
|
|
79
|
+
// standard data items map directly to IMAP atoms
|
|
80
|
+
if (query.uid) {
|
|
81
|
+
queryStructure.push({ type: 'ATOM', value: 'UID' });
|
|
82
|
+
}
|
|
83
|
+
['flags', 'bodyStructure', 'envelope', 'internalDate'].forEach(key => {
|
|
84
|
+
if (items[key]) {
|
|
59
85
|
queryStructure.push({ type: 'ATOM', value: key.toUpperCase() });
|
|
60
86
|
}
|
|
61
87
|
});
|
|
62
|
-
if (
|
|
88
|
+
if (items.size) {
|
|
63
89
|
queryStructure.push({ type: 'ATOM', value: 'RFC822.SIZE' });
|
|
64
90
|
}
|
|
65
91
|
// Fetch full message source, optionally with byte range (start/maxLength)
|
|
@@ -179,18 +205,32 @@ export default async function fetch(connection, range, query, options) {
|
|
|
179
205
|
// (useful for large result sets). Otherwise, collect all into messages.list.
|
|
180
206
|
FETCH: async (untagged) => {
|
|
181
207
|
messages.count++;
|
|
182
|
-
|
|
208
|
+
if (consumerError) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
let formatted = await formatMessageResponse(untagged, mailbox, connection.idHashAlgorithm, connection);
|
|
183
212
|
if (typeof options.onUntaggedFetch === 'function') {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
213
|
+
/* c8 ignore next */ // a UID FETCH row without its UID is a non-compliant server, so the seq fallback is not exercised
|
|
214
|
+
let key = options.uid ? formatted.uid || formatted.seq : formatted.seq;
|
|
215
|
+
if (retryCount && key <= maxDelivered) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
maxDelivered = Math.max(maxDelivered, key);
|
|
219
|
+
try {
|
|
220
|
+
await new Promise((resolve, reject) => {
|
|
221
|
+
options.onUntaggedFetch(formatted, err => {
|
|
222
|
+
if (err) {
|
|
223
|
+
reject(err);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
resolve();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
192
229
|
});
|
|
193
|
-
}
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
consumerError = err;
|
|
233
|
+
}
|
|
194
234
|
}
|
|
195
235
|
else {
|
|
196
236
|
messages.list.push(formatted);
|
|
@@ -199,6 +239,9 @@ export default async function fetch(connection, range, query, options) {
|
|
|
199
239
|
}
|
|
200
240
|
});
|
|
201
241
|
response.next();
|
|
242
|
+
if (consumerError) {
|
|
243
|
+
throw consumerError;
|
|
244
|
+
}
|
|
202
245
|
return messages;
|
|
203
246
|
}
|
|
204
247
|
catch (err) {
|
package/dist/esm/commands/id.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatDateTime } from '../tools.js';
|
|
1
|
+
import { formatDateTime, isUnsafeKey } from '../tools.js';
|
|
2
2
|
/**
|
|
3
3
|
* Sends ID info to the server and updates server info data based on the response.
|
|
4
4
|
*
|
|
@@ -40,7 +40,14 @@ export default async function id(connection, clientInfo) {
|
|
|
40
40
|
key = val.value;
|
|
41
41
|
}
|
|
42
42
|
else if (typeof key === 'string' && typeof val.value === 'string') {
|
|
43
|
-
|
|
43
|
+
// The server picks the keys of this object, which the caller reads
|
|
44
|
+
// back as serverInfo: a prototype-chain name is skipped as it is for
|
|
45
|
+
// every other server-named key, so it can neither be shadowed nor
|
|
46
|
+
// written through
|
|
47
|
+
let name = key.toLowerCase().trim();
|
|
48
|
+
if (!isUnsafeKey(name)) {
|
|
49
|
+
map[name] = val.value;
|
|
50
|
+
}
|
|
44
51
|
}
|
|
45
52
|
});
|
|
46
53
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { guardedPromise, hasCapability, logConnectionError, restampConnectionError, unrefTimer, clearTimer, getSelectedMailbox } from '../tools.js';
|
|
1
|
+
import { guardedPromise, hasCapability, isServerRefusal, logConnectionError, restampConnectionError, unrefTimer, clearTimer, getSelectedMailbox } from '../tools.js';
|
|
2
2
|
const NOOP_INTERVAL = 2 * 60 * 1000;
|
|
3
3
|
/**
|
|
4
4
|
* Marks the connection as idling on behalf of one session and returns a release function.
|
|
@@ -57,8 +57,11 @@ async function runIdle(connection) {
|
|
|
57
57
|
path: connection.mailbox && connection.mailbox.path,
|
|
58
58
|
cid: connection.id
|
|
59
59
|
});
|
|
60
|
-
|
|
60
|
+
// Marked before the write: write() closes the connection when the transport is
|
|
61
|
+
// already gone, and close() breaks IDLE through this very function, which
|
|
62
|
+
// would otherwise write DONE again from inside itself
|
|
61
63
|
doneSent = true;
|
|
64
|
+
connection.write('DONE');
|
|
62
65
|
releaseIdling();
|
|
63
66
|
if (connection.preCheck === ownPreCheck) {
|
|
64
67
|
connection.preCheck = false; // unset itself
|
|
@@ -124,7 +127,10 @@ async function runIdle(connection) {
|
|
|
124
127
|
// A tagged NO or BAD only means the server refused IDLE; the connection is still usable,
|
|
125
128
|
// so the waiters are released by the finally block below and their own commands run.
|
|
126
129
|
// Anything else (close, lost socket, parser failure) fails the waiters too.
|
|
127
|
-
let refusedByServer =
|
|
130
|
+
let refusedByServer = isServerRefusal(err);
|
|
131
|
+
if (refusedByServer) {
|
|
132
|
+
connection.skipIdle = true;
|
|
133
|
+
}
|
|
128
134
|
if (preCheckWaitQueue.length && !refusedByServer) {
|
|
129
135
|
// One error for the whole queue: every waiter failed at the same site, for the same
|
|
130
136
|
// reason. Built inside the guard so a teardown with nothing queued - the common case -
|
|
@@ -310,7 +316,7 @@ export default async function idle(connection, maxIdleTime) {
|
|
|
310
316
|
// If server supports IDLE (RFC 2177, folded into base IMAP4rev2), use it for
|
|
311
317
|
// real-time push notifications. Otherwise, fall back to periodic polling with
|
|
312
318
|
// NOOP/STATUS/SELECT.
|
|
313
|
-
if (hasCapability(connection, 'IDLE')) {
|
|
319
|
+
if (hasCapability(connection, 'IDLE') && !connection.skipIdle) {
|
|
314
320
|
let idleTimer;
|
|
315
321
|
let stillIdling = false;
|
|
316
322
|
// IDLE loop: runs IDLE, and if maxIdleTime is reached, breaks and restarts to keep the
|
|
@@ -334,13 +340,17 @@ export default async function idle(connection, maxIdleTime) {
|
|
|
334
340
|
}
|
|
335
341
|
let resp = await runIdle(connection);
|
|
336
342
|
clearTimeout(idleTimer);
|
|
337
|
-
|
|
343
|
+
// A restart only makes sense with nothing queued behind the break (a CLOSE, say; run()
|
|
344
|
+
// re-arms auto-IDLE once that command is done) and the mailbox still selected on a
|
|
345
|
+
// usable connection
|
|
346
|
+
const canRestart = stillIdling && !connection.requestQueue.length && !!getSelectedMailbox(connection) && connection.usable;
|
|
347
|
+
if (!canRestart) {
|
|
338
348
|
return resp;
|
|
339
349
|
}
|
|
340
350
|
stillIdling = false;
|
|
341
351
|
}
|
|
342
352
|
}
|
|
343
|
-
// Fallback for servers without IDLE support: poll at regular intervals using
|
|
353
|
+
// Fallback for servers without IDLE support, or that refused it: poll at regular intervals using
|
|
344
354
|
// NOOP (default), STATUS, or SELECT depending on missingIdleCommand config.
|
|
345
355
|
return runPollingFallback(connection, maxIdleTime);
|
|
346
356
|
}
|
|
@@ -375,6 +375,15 @@ export default async function list(connection, reference, mailbox, options) {
|
|
|
375
375
|
// Subscribed-only mailboxes that weren't in LIST are intentionally ignored
|
|
376
376
|
// (they may be phantom entries from old subscriptions to deleted mailboxes).
|
|
377
377
|
let runLsub = async () => {
|
|
378
|
+
// LIST has completed, so its entries are indexed once instead of searched per LSUB
|
|
379
|
+
// response, which was quadratic in the folder count. The first entry for a path wins,
|
|
380
|
+
// as with the linear search it replaces
|
|
381
|
+
let entriesByPath = new Map();
|
|
382
|
+
for (let entry of entries) {
|
|
383
|
+
if (!entriesByPath.has(entry.path)) {
|
|
384
|
+
entriesByPath.set(entry.path, entry);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
378
387
|
let response = await connection.exec('LSUB', [encodePath(connection, normalizedReference), encodePath(connection, normalizedMailbox)], {
|
|
379
388
|
untagged: {
|
|
380
389
|
LSUB: async (untagged) => {
|
|
@@ -400,7 +409,7 @@ export default async function list(connection, reference, mailbox, options) {
|
|
|
400
409
|
entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
|
|
401
410
|
entry.name = entry.parent.pop();
|
|
402
411
|
// Merge LSUB data into existing LIST entry if found
|
|
403
|
-
let existing =
|
|
412
|
+
let existing = entriesByPath.get(entry.path);
|
|
404
413
|
if (existing) {
|
|
405
414
|
existing.subscribed = true;
|
|
406
415
|
// Merge any additional flags from LSUB into the LIST entry
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getStatusCode, getErrorText } from '../tools.js';
|
|
1
|
+
import { getStatusCode, getErrorText, isServerRefusal } from '../tools.js';
|
|
2
2
|
/**
|
|
3
3
|
* Authenticates user using the IMAP LOGIN command.
|
|
4
4
|
*
|
|
@@ -30,7 +30,11 @@ export default async function login(connection, username, password) {
|
|
|
30
30
|
if (errorCode) {
|
|
31
31
|
err.serverResponseCode = errorCode;
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
// Only a tagged NO/BAD is the server refusing the credentials; a lost connection or a
|
|
34
|
+
// timeout during LOGIN says nothing about them, and a caller may retry it
|
|
35
|
+
if (isServerRefusal(err)) {
|
|
36
|
+
err.authenticationFailed = true;
|
|
37
|
+
}
|
|
34
38
|
err.response = await getErrorText(err.response);
|
|
35
39
|
throw err;
|
|
36
40
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { clearTimer } from '../tools.js';
|
|
2
|
+
// How long to wait for the server to answer LOGOUT before closing the socket anyway. Without a
|
|
3
|
+
// bound of its own, an unanswered LOGOUT kept logout() pending until the socket timeout.
|
|
4
|
+
const LOGOUT_TIMEOUT = 10 * 1000;
|
|
1
5
|
/**
|
|
2
6
|
* Logs out the user and closes the connection.
|
|
3
7
|
*
|
|
@@ -16,6 +20,8 @@ export default async function logout(connection) {
|
|
|
16
20
|
return false;
|
|
17
21
|
}
|
|
18
22
|
let response;
|
|
23
|
+
// close() rejects the pending LOGOUT with NoConnection, which counts as a completed logout
|
|
24
|
+
let timer = setTimeout(() => connection.close(), LOGOUT_TIMEOUT);
|
|
19
25
|
try {
|
|
20
26
|
response = await connection.exec('LOGOUT');
|
|
21
27
|
return true;
|
|
@@ -33,6 +39,7 @@ export default async function logout(connection) {
|
|
|
33
39
|
// Set state to LOGOUT before closing to prevent any further commands from
|
|
34
40
|
// being queued. The socket is closed unconditionally in this finally block
|
|
35
41
|
// regardless of whether the LOGOUT command succeeded or failed.
|
|
42
|
+
clearTimer(timer);
|
|
36
43
|
connection.state = connection.states.LOGOUT;
|
|
37
44
|
if (response && typeof response.next === 'function') {
|
|
38
45
|
response.next();
|
|
@@ -31,7 +31,8 @@ export default async function namespace(connection) {
|
|
|
31
31
|
}
|
|
32
32
|
let response;
|
|
33
33
|
try {
|
|
34
|
-
|
|
34
|
+
// NIL personal namespaces and a missing NAMESPACE response leave the defaults in place
|
|
35
|
+
let map = { personal: [], other: false, shared: false };
|
|
35
36
|
response = await connection.exec('NAMESPACE', false, {
|
|
36
37
|
untagged: {
|
|
37
38
|
// The NAMESPACE response (RFC 2342) contains exactly three sections:
|
|
@@ -43,19 +44,22 @@ export default async function namespace(connection) {
|
|
|
43
44
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
+
// NIL personal namespaces are legal (RFC 2342 section 5, e.g. after an anonymous login)
|
|
48
|
+
map.personal = getNamsepaceInfo(untagged.attributes[0]) || [];
|
|
47
49
|
map.other = getNamsepaceInfo(untagged.attributes[1]);
|
|
48
50
|
map.shared = getNamsepaceInfo(untagged.attributes[2]);
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
});
|
|
54
|
+
// Release the response before touching the parsed data, so nothing below can leave the
|
|
55
|
+
// reader parked behind this command
|
|
56
|
+
response.next();
|
|
52
57
|
connection.namespaces = map;
|
|
53
58
|
// make sure that we have the first personal namespace always set
|
|
54
59
|
if (!connection.namespaces.personal[0]) {
|
|
55
60
|
connection.namespaces.personal[0] = { prefix: '', delimiter: '.' };
|
|
56
61
|
}
|
|
57
62
|
connection.namespaces.personal[0].prefix = connection.namespaces.personal[0].prefix || '';
|
|
58
|
-
response.next();
|
|
59
63
|
connection.namespace = connection.namespaces.personal[0];
|
|
60
64
|
return connection.namespace;
|
|
61
65
|
}
|
|
@@ -11,7 +11,9 @@ export default async function quota(connection, path) {
|
|
|
11
11
|
// nothing to do here
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
// An RFC 9208 server advertises its QUOTA=RES-* resource types and does not have to list
|
|
15
|
+
// the bare RFC 2087 QUOTA token as well
|
|
16
|
+
if (!connection.capabilities.has('QUOTA') && ![...connection.capabilities.keys()].some(capability => capability.startsWith('QUOTA=RES-'))) {
|
|
15
17
|
return false;
|
|
16
18
|
}
|
|
17
19
|
path = normalizePath(connection, path);
|
|
@@ -21,7 +21,8 @@ export default async function rename(connection, path, newPath) {
|
|
|
21
21
|
// as IMAP servers will not rename an active mailbox.
|
|
22
22
|
let selected = getSelectedMailbox(connection);
|
|
23
23
|
if (selected && selected.path === path) {
|
|
24
|
-
|
|
24
|
+
// UNSELECT where possible, CLOSE would expunge messages flagged \Deleted
|
|
25
|
+
await connection.run('CLOSE', { unselect: true });
|
|
25
26
|
}
|
|
26
27
|
let response;
|
|
27
28
|
try {
|
|
@@ -226,6 +226,11 @@ export default async function select(connection, pathInput, options) {
|
|
|
226
226
|
if (!currentMailbox || currentMailbox.path !== path) {
|
|
227
227
|
emitSafe(connection, 'mailboxOpen', connection.mailbox);
|
|
228
228
|
}
|
|
229
|
+
else if (typeof map.exists === 'number' && map.exists !== currentMailbox.exists) {
|
|
230
|
+
// 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
|
+
emitSafe(connection, 'exists', { path, count: map.exists, prevCount: currentMailbox.exists });
|
|
233
|
+
}
|
|
229
234
|
response.next();
|
|
230
235
|
return map;
|
|
231
236
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { encodePath, normalizePath, buildStatusQueryAttributes, isRev2Active, isAuthenticatedState, emitSafe, getSelectedMailbox } from '../tools.js';
|
|
1
|
+
import { encodePath, normalizePath, buildStatusQueryAttributes, isRev2Active, isAuthenticatedState, emitSafe, getSelectedMailbox, logConnectionError } from '../tools.js';
|
|
2
2
|
import { parseStatusList } from './status-fields.js';
|
|
3
3
|
// STATUS fields that also refresh the live mailbox state when the queried mailbox is the
|
|
4
4
|
// currently selected one. Keyed by the output property name parseStatusList() reports.
|
|
@@ -89,7 +89,12 @@ export default async function status(connection, path, query) {
|
|
|
89
89
|
// Not a deadlock, and only reachable when the server rejects the STATUS, but a polled
|
|
90
90
|
// STATUS of a missing folder ends the poll early.
|
|
91
91
|
if (err.responseStatus === 'NO') {
|
|
92
|
-
|
|
92
|
+
// A failing probe (lost connection, throttling) answers nothing about the mailbox,
|
|
93
|
+
// so STATUS then fails the same way as any other failed STATUS
|
|
94
|
+
let folders = await connection.run('LIST', '', path, { listOnly: true }).catch((listErr) => {
|
|
95
|
+
logConnectionError(connection, 'Failed to check if the mailbox exists', listErr);
|
|
96
|
+
return false;
|
|
97
|
+
});
|
|
93
98
|
if (folders && !folders.length) {
|
|
94
99
|
let error = new Error(`Mailbox doesn't exist: ${path}`);
|
|
95
100
|
error.code = 'NotFound';
|
|
@@ -16,4 +16,4 @@ export interface StoreCommandOptions extends StoreOptions {
|
|
|
16
16
|
* @param options - Store options
|
|
17
17
|
* @returns True on success, false on failure or if nothing to do
|
|
18
18
|
*/
|
|
19
|
-
export default function store(connection: ImapFlow, range: string, flags: string | string[], options
|
|
19
|
+
export default function store(connection: ImapFlow, range: string, flags: string | string[], options?: StoreCommandOptions | undefined): Promise<boolean>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatFlag, canUseFlag, reportCommandError, getSelectedMailbox } from '../tools.js';
|
|
1
|
+
import { formatFlag, canUseFlag, encodePath, reportCommandError, getSelectedMailbox } from '../tools.js';
|
|
2
2
|
/**
|
|
3
3
|
* Updates flags or labels for messages in the selected mailbox.
|
|
4
4
|
*
|
|
@@ -9,13 +9,12 @@ import { formatFlag, canUseFlag, reportCommandError, getSelectedMailbox } from '
|
|
|
9
9
|
* @returns True on success, false on failure or if nothing to do
|
|
10
10
|
*/
|
|
11
11
|
export default async function store(connection, range, flags, options) {
|
|
12
|
+
options = options || {};
|
|
12
13
|
let mailbox = getSelectedMailbox(connection);
|
|
13
14
|
if (!mailbox || !range || (options.useLabels && !connection.capabilities.has('X-GM-EXT-1'))) {
|
|
14
15
|
// nothing to do here
|
|
15
16
|
return false;
|
|
16
17
|
}
|
|
17
|
-
/* c8 ignore next */ // options.useLabels is dereferenced in the guard above, so options is always defined here
|
|
18
|
-
options = options || {};
|
|
19
18
|
// Build the IMAP STORE operation name. The format is:
|
|
20
19
|
// [+|-]FLAGS[.SILENT] or [+|-]X-GM-LABELS
|
|
21
20
|
// Where: no prefix = replace all, + = add, - = remove
|
|
@@ -59,7 +58,10 @@ export default async function store(connection, range, flags, options) {
|
|
|
59
58
|
const dropped = [];
|
|
60
59
|
flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
|
|
61
60
|
.map(flag => {
|
|
62
|
-
|
|
61
|
+
// Gmail labels other than the \-prefixed system labels are mailbox names: astrings in
|
|
62
|
+
// the form mailbox names take on the session (modified UTF-7 unless UTF-8 is enabled),
|
|
63
|
+
// not atoms like IMAP keywords
|
|
64
|
+
let formatted = options.useLabels && flag && flag.charAt(0) !== '\\' ? encodePath(connection, flag) : formatFlag(flag);
|
|
63
65
|
if (!formatted || (!canUseFlag(flagSource, formatted) && operationName !== 'remove')) {
|
|
64
66
|
dropped.push(flag);
|
|
65
67
|
return false;
|
|
@@ -81,13 +83,10 @@ export default async function store(connection, range, flags, options) {
|
|
|
81
83
|
if (!flags.length && !clearAll) {
|
|
82
84
|
return false;
|
|
83
85
|
}
|
|
84
|
-
let attributes = [
|
|
85
|
-
{ type: 'SEQUENCE', value: range },
|
|
86
|
-
{ type: 'ATOM', value: operation },
|
|
87
|
-
flags.map(flag => ({ type: 'ATOM', value: flag }))
|
|
88
|
-
];
|
|
86
|
+
let attributes = [{ type: 'SEQUENCE', value: range }];
|
|
89
87
|
// CONDSTORE (RFC 7162): UNCHANGEDSINCE modifier prevents updating messages whose
|
|
90
88
|
// mod-sequence is higher than the specified value, avoiding overwriting concurrent changes.
|
|
89
|
+
// The store-modifiers list goes between the sequence set and the item name (section 3.1.3).
|
|
91
90
|
if (options.unchangedSince && connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
|
|
92
91
|
attributes.push([
|
|
93
92
|
{
|
|
@@ -100,6 +99,7 @@ export default async function store(connection, range, flags, options) {
|
|
|
100
99
|
}
|
|
101
100
|
]);
|
|
102
101
|
}
|
|
102
|
+
attributes.push({ type: 'ATOM', value: operation }, flags.map(flag => ({ type: 'ATOM', value: flag })));
|
|
103
103
|
let response;
|
|
104
104
|
try {
|
|
105
105
|
response = await connection.exec(options.uid ? 'UID STORE' : 'STORE', attributes);
|