imapflow 1.4.8 → 1.5.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/.github/workflows/test.yml +20 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -0
- package/CLAUDE.md +12 -5
- package/Gruntfile.js +3 -1
- package/lib/commands/authenticate.js +8 -3
- package/lib/commands/enable.js +13 -4
- package/lib/commands/expunge.js +2 -2
- package/lib/commands/fetch.js +18 -14
- package/lib/commands/idle.js +6 -3
- package/lib/commands/list.js +241 -61
- package/lib/commands/move.js +2 -2
- package/lib/commands/namespace.js +3 -1
- package/lib/commands/search.js +88 -13
- package/lib/commands/status.js +19 -26
- package/lib/handler/imap-compiler.js +12 -9
- package/lib/handler/token-parser.js +7 -0
- package/lib/imap-flow.d.ts +19 -3
- package/lib/imap-flow.js +58 -9
- package/lib/search-compiler.js +15 -1
- package/lib/tools.js +173 -9
- package/package.json +3 -2
- package/test/commands-branches-test.js +11 -4
- package/test/commands-integration-test.js +1528 -108
- package/test/connection-edge-cases-test.js +4 -40
- package/test/fixtures/test-tls.js +2 -2
- package/test/handler-branches-test.js +4 -3
- package/test/imap-compiler-test.js +85 -0
- package/test/imap-flow-coverage-test.js +8 -1
- package/test/imap-flow-fetch-download-test.js +57 -4
- package/test/imap-flow-internals-test.js +2 -2
- package/test/imap-flow-methods-test.js +65 -6
- package/test/imap-flow-secure-test.js +25 -11
- package/test/imap-flow-server-test.js +80 -0
- package/test/imap-parser-test.js +113 -3
- package/test/imap-stream-test.js +46 -0
- package/test/integration/README.md +52 -0
- package/test/integration/dovecot-test.conf +27 -0
- package/test/integration/rev2-live-test.js +367 -0
- package/test/integration/run-rev2-tests.sh +75 -0
- package/test/reliability-improvements-test.js +4 -1
- package/test/search-compiler-test.js +36 -0
- package/test/search-test.js +52 -54
- package/test/tools-test.js +176 -19
package/lib/commands/status.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { encodePath, normalizePath } = require('../tools.js');
|
|
3
|
+
const { encodePath, normalizePath, buildStatusQueryAttributes, isRev2Active } = require('../tools.js');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Requests status information about a mailbox.
|
|
@@ -24,33 +24,18 @@ module.exports = async (connection, path, query) => {
|
|
|
24
24
|
// otherwise use unquoted ATOM. Same approach as in SELECT.
|
|
25
25
|
let attributes = [{ type: encodedPath.indexOf('&') >= 0 ? 'STRING' : 'ATOM', value: encodedPath }];
|
|
26
26
|
|
|
27
|
-
// Build the list of STATUS data items the caller wants
|
|
28
|
-
|
|
29
|
-
let queryAttributes = [];
|
|
30
|
-
Object.keys(query || {}).forEach(key => {
|
|
31
|
-
if (!query[key]) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
27
|
+
// Build the list of STATUS data items the caller wants
|
|
28
|
+
let queryAttributes = buildStatusQueryAttributes(connection, query);
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
case 'UIDVALIDITY':
|
|
40
|
-
case 'UNSEEN':
|
|
41
|
-
queryAttributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
|
42
|
-
break;
|
|
43
|
-
|
|
44
|
-
case 'HIGHESTMODSEQ':
|
|
45
|
-
if (connection.capabilities.has('CONDSTORE')) {
|
|
46
|
-
queryAttributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
|
47
|
-
}
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
});
|
|
30
|
+
// RECENT does not exist in IMAP4rev2 so it is never requested from a rev2
|
|
31
|
+
// session; its defined value there is always 0. Synthesizing it keeps the
|
|
32
|
+
// return shape identical to a rev1 session for the same query.
|
|
33
|
+
let syntheticRecent = query && query.recent && isRev2Active(connection);
|
|
51
34
|
|
|
52
35
|
if (!queryAttributes.length) {
|
|
53
|
-
|
|
36
|
+
// A query that only contained items unavailable on this session - the
|
|
37
|
+
// caller still gets a status object if every such item has a defined value
|
|
38
|
+
return syntheticRecent ? { path, recent: 0 } : false;
|
|
54
39
|
}
|
|
55
40
|
|
|
56
41
|
attributes.push(queryAttributes);
|
|
@@ -101,7 +86,12 @@ module.exports = async (connection, path, query) => {
|
|
|
101
86
|
updateMailbox: (val, conn) => {
|
|
102
87
|
conn.mailbox.highestModseq = val;
|
|
103
88
|
}
|
|
104
|
-
}
|
|
89
|
+
},
|
|
90
|
+
// IMAP4rev2 additions (RFC 9051): total mailbox size in octets
|
|
91
|
+
// (number64, exact as a JS number up to 2^53-1) and count of
|
|
92
|
+
// messages with the \Deleted flag
|
|
93
|
+
SIZE: { key: 'size', parser: Number },
|
|
94
|
+
DELETED: { key: 'deleted', parser: Number }
|
|
105
95
|
};
|
|
106
96
|
|
|
107
97
|
let key;
|
|
@@ -134,6 +124,9 @@ module.exports = async (connection, path, query) => {
|
|
|
134
124
|
}
|
|
135
125
|
});
|
|
136
126
|
response.next();
|
|
127
|
+
if (syntheticRecent) {
|
|
128
|
+
map.recent = 0;
|
|
129
|
+
}
|
|
137
130
|
return map;
|
|
138
131
|
} catch (err) {
|
|
139
132
|
// A NO response usually means the mailbox doesn't exist. Verify by
|
|
@@ -130,15 +130,18 @@ module.exports = async (response, options) => {
|
|
|
130
130
|
if (isLogging) {
|
|
131
131
|
resp.push(formatRespEntry('"(* ' + node.value.length + 'B literal *)"'));
|
|
132
132
|
} else {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
133
|
+
// The literal size marker counts octets - string values are written as
|
|
134
|
+
// UTF-8, so their UTF-16 .length would undercount multi-byte characters
|
|
135
|
+
let literalLength = !node.value ? 0 : Buffer.isBuffer(node.value) ? node.value.length : Buffer.byteLength(node.value.toString());
|
|
136
|
+
|
|
137
|
+
// Append '+' to the size marker only when the extension actually permits a
|
|
138
|
+
// non-synchronizing literal of this size (RFC 7888): LITERAL+ always,
|
|
139
|
+
// LITERAL- only up to 4096 bytes
|
|
140
|
+
let usePlus = literalPlus || (literalMinus && literalLength <= 4096);
|
|
141
|
+
// canAppend: whether the literal data can be sent in the same buffer segment -
|
|
142
|
+
// non-synchronizing literals always, and everything in single-buffer mode
|
|
143
|
+
// (asArray false), which has no continuation flow
|
|
144
|
+
let canAppend = !asArray || usePlus;
|
|
142
145
|
|
|
143
146
|
// Emit the literal header: optional '~' prefix for literal8, then {size[+]}\r\n
|
|
144
147
|
resp.push(formatRespEntry(`${node.isLiteral8 ? '~' : ''}{${literalLength}${usePlus ? '+' : ''}}\r\n`));
|
|
@@ -580,6 +580,13 @@ class TokenParser {
|
|
|
580
580
|
if (!this.currentNode.literalLength) {
|
|
581
581
|
// special case where literal content length is 0
|
|
582
582
|
// close the node right away, do not wait for additional input
|
|
583
|
+
if (this.options.literals && this.options.literals.length) {
|
|
584
|
+
// ImapStream queues a Buffer for every literal marker it
|
|
585
|
+
// extracts, including {0} - consume the queue entry so
|
|
586
|
+
// subsequent literals in the same response stay aligned
|
|
587
|
+
// with their markers instead of shifting by one
|
|
588
|
+
this.currentNode.value = this.options.literals.shift();
|
|
589
|
+
}
|
|
583
590
|
this.currentNode.endPos = this.pos + i;
|
|
584
591
|
this.currentNode.isClosed = true;
|
|
585
592
|
this.currentNode = this.currentNode.parentNode;
|
package/lib/imap-flow.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface ImapFlowOptions {
|
|
|
54
54
|
disableBinary?: boolean;
|
|
55
55
|
/** If true, do not enable supported extensions */
|
|
56
56
|
disableAutoEnable?: boolean;
|
|
57
|
+
/** If true, do not enable IMAP4rev2 mode even if the server supports it */
|
|
58
|
+
disableIMAP4rev2?: boolean;
|
|
57
59
|
/** How long to wait for the connection to be established. Defaults to 90 seconds */
|
|
58
60
|
connectionTimeout?: number;
|
|
59
61
|
/** How long to wait for the greeting. Defaults to 16 seconds */
|
|
@@ -103,7 +105,7 @@ export interface MailboxObject {
|
|
|
103
105
|
specialUse?: string;
|
|
104
106
|
/** True if mailbox was found from the output of LIST command */
|
|
105
107
|
listed?: boolean;
|
|
106
|
-
/** True if mailbox
|
|
108
|
+
/** True if the mailbox is subscribed - reported by LSUB or by LIST RETURN (SUBSCRIBED) on LIST-EXTENDED/IMAP4rev2 servers */
|
|
107
109
|
subscribed?: boolean;
|
|
108
110
|
/** A Set of flags available to use in this mailbox. If it is not set or includes special flag "\*" then any flag can be used */
|
|
109
111
|
permanentFlags?: Set<string>;
|
|
@@ -184,7 +186,7 @@ export interface ListResponse {
|
|
|
184
186
|
specialUse?: string;
|
|
185
187
|
/** True if mailbox was found from the output of LIST command */
|
|
186
188
|
listed: boolean;
|
|
187
|
-
/** True if mailbox
|
|
189
|
+
/** True if the mailbox is subscribed - reported by LSUB or by LIST RETURN (SUBSCRIBED) on LIST-EXTENDED/IMAP4rev2 servers */
|
|
188
190
|
subscribed: boolean;
|
|
189
191
|
/** If statusQuery was used, then this value includes the status response */
|
|
190
192
|
status?: StatusObject;
|
|
@@ -205,6 +207,10 @@ export interface ListOptions {
|
|
|
205
207
|
unseen?: boolean;
|
|
206
208
|
/** If true request last known modseq value */
|
|
207
209
|
highestModseq?: boolean;
|
|
210
|
+
/** If true request total mailbox size in octets (requires STATUS=SIZE or IMAP4rev2) */
|
|
211
|
+
size?: boolean;
|
|
212
|
+
/** If true request count of messages with \Deleted flag (requires IMAP4rev2) */
|
|
213
|
+
deleted?: boolean;
|
|
208
214
|
};
|
|
209
215
|
/** Set specific paths as special use folders */
|
|
210
216
|
specialUseHints?: {
|
|
@@ -234,7 +240,7 @@ export interface ListTreeResponse {
|
|
|
234
240
|
specialUse?: string;
|
|
235
241
|
/** True if mailbox was found from the output of LIST command */
|
|
236
242
|
listed?: boolean;
|
|
237
|
-
/** True if mailbox
|
|
243
|
+
/** True if the mailbox is subscribed - reported by LSUB or by LIST RETURN (SUBSCRIBED) on LIST-EXTENDED/IMAP4rev2 servers */
|
|
238
244
|
subscribed?: boolean;
|
|
239
245
|
/** If true then this mailbox can not be selected in the UI */
|
|
240
246
|
disabled?: boolean;
|
|
@@ -280,6 +286,10 @@ export interface StatusObject {
|
|
|
280
286
|
unseen?: number;
|
|
281
287
|
/** Last known modseq value (if CONDSTORE extension is enabled) */
|
|
282
288
|
highestModseq?: bigint;
|
|
289
|
+
/** Total size of the mailbox in octets (only if requested and the server supports STATUS=SIZE or IMAP4rev2) */
|
|
290
|
+
size?: number;
|
|
291
|
+
/** Count of messages with \Deleted flag (only if requested and IMAP4rev2 is active) */
|
|
292
|
+
deleted?: number;
|
|
283
293
|
}
|
|
284
294
|
|
|
285
295
|
export type SequenceString = string | number | bigint;
|
|
@@ -491,6 +501,8 @@ export interface FetchMessageObject {
|
|
|
491
501
|
internalDate?: Date | string;
|
|
492
502
|
/** A Map of message body parts where key is requested part identifier and value is a Buffer */
|
|
493
503
|
bodyParts?: Map<string, Buffer>;
|
|
504
|
+
/** Part identifiers from bodyParts that arrived via FETCH BINARY, i.e. with the content-transfer-encoding already decoded by the server */
|
|
505
|
+
binaryParts?: Set<string>;
|
|
494
506
|
/** Requested header lines as Buffer */
|
|
495
507
|
headers?: Buffer;
|
|
496
508
|
/** Account unique ID for this email */
|
|
@@ -753,6 +765,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
753
765
|
uidValidity?: boolean;
|
|
754
766
|
unseen?: boolean;
|
|
755
767
|
highestModseq?: boolean;
|
|
768
|
+
/** Requires STATUS=SIZE or IMAP4rev2 */
|
|
769
|
+
size?: boolean;
|
|
770
|
+
/** Requires IMAP4rev2 */
|
|
771
|
+
deleted?: boolean;
|
|
756
772
|
}
|
|
757
773
|
): Promise<StatusObject>;
|
|
758
774
|
|
package/lib/imap-flow.js
CHANGED
|
@@ -35,7 +35,8 @@ const {
|
|
|
35
35
|
normalizePath,
|
|
36
36
|
expandRange,
|
|
37
37
|
AuthenticationFailure,
|
|
38
|
-
getColorFlags
|
|
38
|
+
getColorFlags,
|
|
39
|
+
hasCapability
|
|
39
40
|
} = require('./tools');
|
|
40
41
|
|
|
41
42
|
const imapCommands = require('./imap-commands.js');
|
|
@@ -69,7 +70,7 @@ const states = {
|
|
|
69
70
|
* @property {Set<string>} flags list of flags for this mailbox
|
|
70
71
|
* @property {String} [specialUse] one of special-use flags (if applicable): "\All", "\Archive", "\Drafts", "\Flagged", "\Junk", "\Sent", "\Trash". Additionally INBOX has non-standard "\Inbox" flag set
|
|
71
72
|
* @property {Boolean} listed `true` if mailbox was found from the output of LIST command
|
|
72
|
-
* @property {Boolean} subscribed `true` if mailbox
|
|
73
|
+
* @property {Boolean} subscribed `true` if the mailbox is subscribed - reported by LSUB or by LIST RETURN (SUBSCRIBED) on LIST-EXTENDED/IMAP4rev2 servers
|
|
73
74
|
* @property {Set<string>} permanentFlags A Set of flags available to use in this mailbox. If it is not set or includes special flag "\\\*" then any flag can be used.
|
|
74
75
|
* @property {String} [mailboxId] unique mailbox ID if server has `OBJECTID` extension enabled
|
|
75
76
|
* @property {BigInt} [highestModseq] latest known modseq value if server has CONDSTORE or XYMHIGHESTMODSEQ enabled
|
|
@@ -221,6 +222,11 @@ class ImapFlow extends EventEmitter {
|
|
|
221
222
|
* @property {Boolean} [disableAutoEnable=false]
|
|
222
223
|
* If `true`, do not automatically enable supported IMAP extensions.
|
|
223
224
|
*
|
|
225
|
+
* @property {Boolean} [disableIMAP4rev2=false]
|
|
226
|
+
* If `true`, do not enable IMAP4rev2 mode even if the server supports it.
|
|
227
|
+
* Use as a targeted opt-out for servers with broken IMAP4rev2 implementations
|
|
228
|
+
* without losing the other auto-enabled extensions.
|
|
229
|
+
*
|
|
224
230
|
* @property {Number} [connectionTimeout=90000]
|
|
225
231
|
* Maximum time (in milliseconds) to wait for the connection to establish. Defaults to 90 seconds.
|
|
226
232
|
*
|
|
@@ -397,6 +403,14 @@ class ImapFlow extends EventEmitter {
|
|
|
397
403
|
|
|
398
404
|
this.disableBinary = !!this.options.disableBinary;
|
|
399
405
|
|
|
406
|
+
// Set when the server rejects a LIST RETURN option group, the auxiliary
|
|
407
|
+
// SPECIAL-USE/CHILDREN return options, or the LSUB command, so later
|
|
408
|
+
// listings on this connection skip what the server does not support
|
|
409
|
+
this.skipListSubscribedArg = false;
|
|
410
|
+
this.skipListStatusArgs = false;
|
|
411
|
+
this.skipListAuxArgs = false;
|
|
412
|
+
this.skipLsub = false;
|
|
413
|
+
|
|
400
414
|
// Named error handler for proper cleanup. Certain error codes represent
|
|
401
415
|
// expected socket/network issues (buffer exhaustion, connection reset, broken pipe,
|
|
402
416
|
// timeout, unreachable host) that just need a silent connection close rather
|
|
@@ -568,7 +582,8 @@ class ImapFlow extends EventEmitter {
|
|
|
568
582
|
// are stored in this.commandParts and sent after server "+" continuations.
|
|
569
583
|
let compiled = await compiler(data, {
|
|
570
584
|
asArray: true,
|
|
571
|
-
|
|
585
|
+
// LITERAL- is part of base IMAP4rev2
|
|
586
|
+
literalMinus: hasCapability(this, 'LITERAL-') || this.capabilities.has('LITERAL+')
|
|
572
587
|
});
|
|
573
588
|
this.commandParts = compiled;
|
|
574
589
|
|
|
@@ -1053,13 +1068,29 @@ class ImapFlow extends EventEmitter {
|
|
|
1053
1068
|
}
|
|
1054
1069
|
|
|
1055
1070
|
if (!this.options.disableAutoEnable) {
|
|
1056
|
-
|
|
1057
|
-
await this.run('ENABLE', ['CONDSTORE', 'UTF8=ACCEPT'].concat(this.options.qresync ? 'QRESYNC' : []));
|
|
1071
|
+
await this.autoEnable();
|
|
1058
1072
|
}
|
|
1059
1073
|
|
|
1060
1074
|
this.usable = true;
|
|
1061
1075
|
}
|
|
1062
1076
|
|
|
1077
|
+
// Enable extensions if possible. IMAP4rev2 must be enabled explicitly on
|
|
1078
|
+
// servers that advertise both rev1 and rev2 (RFC 9051 Appendix A); a single
|
|
1079
|
+
// ENABLE call is used so the enabled set is built in one round trip.
|
|
1080
|
+
async autoEnable() {
|
|
1081
|
+
let enableList = ['CONDSTORE', 'UTF8=ACCEPT'].concat(this.options.qresync ? 'QRESYNC' : []).concat(this.options.disableIMAP4rev2 ? [] : 'IMAP4rev2');
|
|
1082
|
+
let enableResult = await this.run('ENABLE', enableList);
|
|
1083
|
+
if (enableResult === false && enableList.includes('IMAP4rev2')) {
|
|
1084
|
+
// RFC 5161 requires servers to ignore unknown ENABLE arguments, but a
|
|
1085
|
+
// broken implementation may reject the whole command over IMAP4rev2 -
|
|
1086
|
+
// retry without it so CONDSTORE/QRESYNC are not lost as collateral
|
|
1087
|
+
await this.run(
|
|
1088
|
+
'ENABLE',
|
|
1089
|
+
enableList.filter(extension => extension !== 'IMAP4rev2')
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1063
1094
|
async compress() {
|
|
1064
1095
|
if (!(await this.run('COMPRESS'))) {
|
|
1065
1096
|
return; // was not able to negotiate compression
|
|
@@ -1510,6 +1541,9 @@ class ImapFlow extends EventEmitter {
|
|
|
1510
1541
|
return;
|
|
1511
1542
|
}
|
|
1512
1543
|
this.state = this.states.AUTHENTICATED;
|
|
1544
|
+
// documented contract for the `authenticated` property: `true` when the
|
|
1545
|
+
// connection was authenticated by a PREAUTH greeting (no credentials known)
|
|
1546
|
+
this.authenticated = true;
|
|
1513
1547
|
this.beginSession(err => {
|
|
1514
1548
|
this.log.error({ err, cid: this.id });
|
|
1515
1549
|
this.closeAfter();
|
|
@@ -2213,7 +2247,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2213
2247
|
* @property {Set<string>} flags a set of flags for this mailbox
|
|
2214
2248
|
* @property {String} specialUse one of special-use flags (if applicable): "\All", "\Archive", "\Drafts", "\Flagged", "\Junk", "\Sent", "\Trash". Additionally INBOX has non-standard "\Inbox" flag set
|
|
2215
2249
|
* @property {Boolean} listed `true` if mailbox was found from the output of LIST command
|
|
2216
|
-
* @property {Boolean} subscribed `true` if mailbox
|
|
2250
|
+
* @property {Boolean} subscribed `true` if the mailbox is subscribed - reported by LSUB or by LIST RETURN (SUBSCRIBED) on LIST-EXTENDED/IMAP4rev2 servers
|
|
2217
2251
|
* @property {StatusObject} [status] If `statusQuery` was used, then this value includes the status response
|
|
2218
2252
|
*/
|
|
2219
2253
|
|
|
@@ -2227,6 +2261,8 @@ class ImapFlow extends EventEmitter {
|
|
|
2227
2261
|
* @property {Boolean} [statusQuery.uidValidity] if `true` request mailbox `UIDVALIDITY` value
|
|
2228
2262
|
* @property {Boolean} [statusQuery.unseen] if `true` request count of unseen messages
|
|
2229
2263
|
* @property {Boolean} [statusQuery.highestModseq] if `true` request last known modseq value
|
|
2264
|
+
* @property {Boolean} [statusQuery.size] if `true` request total mailbox size in octets (requires STATUS=SIZE or IMAP4rev2)
|
|
2265
|
+
* @property {Boolean} [statusQuery.deleted] if `true` request count of messages with \\Deleted flag (requires IMAP4rev2)
|
|
2230
2266
|
* @property {Object} [specialUseHints] set specific paths as special use folders, this would override special use flags provided from the server
|
|
2231
2267
|
* @property {String} [specialUseHints.sent] Path to "Sent Mail" folder
|
|
2232
2268
|
* @property {String} [specialUseHints.trash] Path to "Trash" folder
|
|
@@ -2261,9 +2297,10 @@ class ImapFlow extends EventEmitter {
|
|
|
2261
2297
|
* @property {Set<string>} flags list of flags for this mailbox
|
|
2262
2298
|
* @property {String} specialUse one of special-use flags (if applicable): "\All", "\Archive", "\Drafts", "\Flagged", "\Junk", "\Sent", "\Trash". Additionally INBOX has non-standard "\Inbox" flag set
|
|
2263
2299
|
* @property {Boolean} listed `true` if mailbox was found from the output of LIST command
|
|
2264
|
-
* @property {Boolean} subscribed `true` if mailbox
|
|
2300
|
+
* @property {Boolean} subscribed `true` if the mailbox is subscribed - reported by LSUB or by LIST RETURN (SUBSCRIBED) on LIST-EXTENDED/IMAP4rev2 servers
|
|
2265
2301
|
* @property {Boolean} disabled If `true` then this mailbox can not be selected in the UI
|
|
2266
2302
|
* @property {ListTreeResponse[]} folders An array of subfolders
|
|
2303
|
+
* @property {StatusObject} [status] If `statusQuery` was used, then this value includes the status response
|
|
2267
2304
|
*/
|
|
2268
2305
|
|
|
2269
2306
|
/**
|
|
@@ -2430,6 +2467,8 @@ class ImapFlow extends EventEmitter {
|
|
|
2430
2467
|
* @property {BigInt} [uidValidity] Mailbox `UIDVALIDITY` value
|
|
2431
2468
|
* @property {Number} [unseen] Count of unseen messages
|
|
2432
2469
|
* @property {BigInt} [highestModseq] Last known modseq value (if CONDSTORE extension is enabled)
|
|
2470
|
+
* @property {Number} [size] Total size of the mailbox in octets (only if requested and the server supports STATUS=SIZE or IMAP4rev2)
|
|
2471
|
+
* @property {Number} [deleted] Count of messages with \\Deleted flag (only if requested and IMAP4rev2 is active)
|
|
2433
2472
|
*/
|
|
2434
2473
|
|
|
2435
2474
|
/**
|
|
@@ -2443,6 +2482,8 @@ class ImapFlow extends EventEmitter {
|
|
|
2443
2482
|
* @param {Boolean} query.uidValidity if `true` request mailbox `UIDVALIDITY` value
|
|
2444
2483
|
* @param {Boolean} query.unseen if `true` request count of unseen messages
|
|
2445
2484
|
* @param {Boolean} query.highestModseq if `true` request last known modseq value
|
|
2485
|
+
* @param {Boolean} query.size if `true` request total mailbox size in octets (requires STATUS=SIZE or IMAP4rev2)
|
|
2486
|
+
* @param {Boolean} query.deleted if `true` request count of messages with \\Deleted flag (requires IMAP4rev2)
|
|
2446
2487
|
* @returns {Promise<StatusObject>} status of the indicated mailbox
|
|
2447
2488
|
*
|
|
2448
2489
|
* @example
|
|
@@ -2937,6 +2978,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2937
2978
|
* @property {MessageStructureObject} [bodyStructure] message body structure
|
|
2938
2979
|
* @property {Date} [internalDate] message internal date
|
|
2939
2980
|
* @property {Map<string, Buffer>} [bodyParts] a Map of message body parts where key is requested part identifier and value is a Buffer
|
|
2981
|
+
* @property {Set<string>} [binaryParts] part identifiers from `bodyParts` that arrived via FETCH BINARY, i.e. with the content-transfer-encoding already decoded by the server
|
|
2940
2982
|
* @property {Buffer} [headers] Requested header lines as Buffer
|
|
2941
2983
|
*/
|
|
2942
2984
|
|
|
@@ -3371,7 +3413,11 @@ class ImapFlow extends EventEmitter {
|
|
|
3371
3413
|
// 4. Byte limiter (enforces maxBytes cap)
|
|
3372
3414
|
// `stream` is the head of the pipeline (where raw chunks are written),
|
|
3373
3415
|
// `output` is the tail (what the caller reads from).
|
|
3374
|
-
|
|
3416
|
+
// Parts that arrived via FETCH BINARY (response.binaryParts) are already
|
|
3417
|
+
// decoded by the server - decoding again would corrupt the data, so stage 1
|
|
3418
|
+
// is skipped for them.
|
|
3419
|
+
let clientEncoding = response.binaryParts && response.binaryParts.has(part) ? false : meta.encoding;
|
|
3420
|
+
switch (clientEncoding) {
|
|
3375
3421
|
case 'base64':
|
|
3376
3422
|
output = stream = new libbase64.Decoder();
|
|
3377
3423
|
break;
|
|
@@ -3681,7 +3727,10 @@ class ImapFlow extends EventEmitter {
|
|
|
3681
3727
|
for (let part of Object.keys(data)) {
|
|
3682
3728
|
let meta = data[part].meta;
|
|
3683
3729
|
|
|
3684
|
-
|
|
3730
|
+
// parts that arrived via FETCH BINARY (response.binaryParts) are already
|
|
3731
|
+
// decoded by the server - decoding again would corrupt the data
|
|
3732
|
+
let clientEncoding = response.binaryParts && response.binaryParts.has(part) ? false : meta.encoding;
|
|
3733
|
+
switch (clientEncoding) {
|
|
3685
3734
|
case 'base64':
|
|
3686
3735
|
data[part].content = data[part].content ? libbase64.decode(data[part].content.toString()) : null;
|
|
3687
3736
|
break;
|
package/lib/search-compiler.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
-
const { formatDate, formatFlag, canUseFlag, isDate } = require('./tools.js');
|
|
5
|
+
const { formatDate, formatFlag, canUseFlag, isDate, isRev2Active } = require('./tools.js');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Sets a boolean flag in the IMAP search attributes.
|
|
@@ -186,10 +186,24 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
186
186
|
|
|
187
187
|
// Simple boolean flags without UN- support
|
|
188
188
|
case 'ALL':
|
|
189
|
+
if (params[term]) {
|
|
190
|
+
setBoolOpt(attributes, term, true);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
|
|
189
194
|
case 'NEW':
|
|
190
195
|
case 'OLD':
|
|
191
196
|
case 'RECENT':
|
|
192
197
|
if (params[term]) {
|
|
198
|
+
// The \Recent flag and the NEW/OLD/RECENT search keys were
|
|
199
|
+
// removed in IMAP4rev2 (RFC 9051) - a rev2 session would
|
|
200
|
+
// reject the whole search with a tagged BAD, so fail with a
|
|
201
|
+
// descriptive error instead
|
|
202
|
+
if (isRev2Active(connection)) {
|
|
203
|
+
let error = new Error(`The "${term.toLowerCase()}" search key does not exist in IMAP4rev2`);
|
|
204
|
+
error.code = 'MissingServerExtension';
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
193
207
|
setBoolOpt(attributes, term, true);
|
|
194
208
|
}
|
|
195
209
|
break;
|