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.
Files changed (44) hide show
  1. package/.github/workflows/test.yml +20 -0
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +15 -0
  4. package/CLAUDE.md +12 -5
  5. package/Gruntfile.js +3 -1
  6. package/lib/commands/authenticate.js +8 -3
  7. package/lib/commands/enable.js +13 -4
  8. package/lib/commands/expunge.js +2 -2
  9. package/lib/commands/fetch.js +18 -14
  10. package/lib/commands/idle.js +6 -3
  11. package/lib/commands/list.js +241 -61
  12. package/lib/commands/move.js +2 -2
  13. package/lib/commands/namespace.js +3 -1
  14. package/lib/commands/search.js +88 -13
  15. package/lib/commands/status.js +19 -26
  16. package/lib/handler/imap-compiler.js +12 -9
  17. package/lib/handler/token-parser.js +7 -0
  18. package/lib/imap-flow.d.ts +19 -3
  19. package/lib/imap-flow.js +58 -9
  20. package/lib/search-compiler.js +15 -1
  21. package/lib/tools.js +173 -9
  22. package/package.json +3 -2
  23. package/test/commands-branches-test.js +11 -4
  24. package/test/commands-integration-test.js +1528 -108
  25. package/test/connection-edge-cases-test.js +4 -40
  26. package/test/fixtures/test-tls.js +2 -2
  27. package/test/handler-branches-test.js +4 -3
  28. package/test/imap-compiler-test.js +85 -0
  29. package/test/imap-flow-coverage-test.js +8 -1
  30. package/test/imap-flow-fetch-download-test.js +57 -4
  31. package/test/imap-flow-internals-test.js +2 -2
  32. package/test/imap-flow-methods-test.js +65 -6
  33. package/test/imap-flow-secure-test.js +25 -11
  34. package/test/imap-flow-server-test.js +80 -0
  35. package/test/imap-parser-test.js +113 -3
  36. package/test/imap-stream-test.js +46 -0
  37. package/test/integration/README.md +52 -0
  38. package/test/integration/dovecot-test.conf +27 -0
  39. package/test/integration/rev2-live-test.js +367 -0
  40. package/test/integration/run-rev2-tests.sh +75 -0
  41. package/test/reliability-improvements-test.js +4 -1
  42. package/test/search-compiler-test.js +36 -0
  43. package/test/search-test.js +52 -54
  44. package/test/tools-test.js +176 -19
@@ -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
- // HIGHESTMODSEQ requires the CONDSTORE extension to be available.
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
- switch (key.toUpperCase()) {
36
- case 'MESSAGES':
37
- case 'RECENT':
38
- case 'UIDNEXT':
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
- return false;
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
- let literalLength = !node.value ? 0 : Math.max(node.value.length, 0);
134
-
135
- // canAppend: whether the literal data can be sent in the same buffer segment.
136
- // With LITERAL+ (RFC 7888) the client does not wait for a continuation response.
137
- // With LITERAL- (RFC 7888) the client can skip the wait only for literals <= 4096 bytes.
138
- // When asArray is false we always append inline (single-buffer mode).
139
- let canAppend = !asArray || literalPlus || (literalMinus && literalLength <= 4096);
140
- // Append '+' to the size marker when using LITERAL+ or LITERAL- (non-synchronizing)
141
- let usePlus = canAppend && (literalMinus || literalPlus);
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;
@@ -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 was found from the output of LSUB command */
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 was found from the output of LSUB command */
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 was found from the output of LSUB command */
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 was found from the output of LSUB command
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
- literalMinus: this.capabilities.has('LITERAL-') || this.capabilities.has('LITERAL+')
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
- // enable extensions if possible
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 was found from the output of LSUB command
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 was found from the output of LSUB command
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
- switch (meta.encoding) {
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
- switch (meta.encoding) {
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;
@@ -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;