imapflow 1.2.13 → 1.2.14

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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.2.13"
2
+ ".": "1.2.14"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.14](https://github.com/postalsys/imapflow/compare/v1.2.13...v1.2.14) (2026-03-15)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * correct getColorFlags treating red (index 0) as falsy and use nullish coalescing in resolveCharset ([268629c](https://github.com/postalsys/imapflow/commit/268629cf799027bf802c0231675cdc2c94a227d4))
9
+ * use nullish coalescing in getFlagColor, match euc-jp in getDecoder, and handle BigInt(0) in uidValidity checks ([713e100](https://github.com/postalsys/imapflow/commit/713e100093edff21df020942a2c17e7b9939b42b))
10
+
3
11
  ## [1.2.13](https://github.com/postalsys/imapflow/compare/v1.2.12...v1.2.13) (2026-03-09)
4
12
 
5
13
 
package/lib/charsets.js CHANGED
@@ -291,8 +291,5 @@ CHARACTER_SETS.forEach(entry => {
291
291
  // Returns null if the charset is not recognized.
292
292
  module.exports.resolveCharset = charset => {
293
293
  let key = charset.replace(/[_-\s]/g, '').toLowerCase();
294
- if (CHARSET_MAP.has(key)) {
295
- return CHARSET_MAP.get(key);
296
- }
297
- return null;
294
+ return CHARSET_MAP.get(key) ?? null;
298
295
  };
@@ -111,7 +111,7 @@ module.exports = async (connection, destination, content, flags, idate) => {
111
111
  if (responseCode.toUpperCase() === 'APPENDUID') {
112
112
  let uidValidity = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
113
113
  let uid = section[2] && typeof section[2].value === 'string' && !isNaN(section[2].value) ? Number(section[2].value) : false;
114
- if (uidValidity) {
114
+ if (uidValidity !== false) {
115
115
  map.uidValidity = uidValidity;
116
116
  }
117
117
  if (uid) {
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { normalizePath, encodePath, expandRange, enhanceCommandError } = require('../tools.js');
3
+ const { normalizePath, encodePath, enhanceCommandError } = require('../tools.js');
4
+ const { parseCopyUid } = require('./copyuid-parser.js');
4
5
 
5
6
  /**
6
7
  * Copies messages from the current mailbox to another mailbox.
@@ -35,29 +36,7 @@ module.exports = async (connection, range, destination, options) => {
35
36
 
36
37
  // UIDPLUS (RFC 4315): the server may include a COPYUID response code in the
37
38
  // tagged OK response, providing a mapping from source UIDs to destination UIDs.
38
- // Format: [COPYUID <uidValidity> <sourceUids> <destUids>]
39
- let section = response.response.attributes && response.response.attributes[0] && response.response.attributes[0].section;
40
- let responseCode = section && section.length && section[0] && typeof section[0].value === 'string' ? section[0].value : '';
41
- switch (responseCode) {
42
- case 'COPYUID':
43
- {
44
- // section[1] = destination mailbox UIDVALIDITY
45
- let uidValidity = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
46
- if (uidValidity) {
47
- map.uidValidity = uidValidity;
48
- }
49
-
50
- // section[2] = source UID set, section[3] = destination UID set
51
- // Both can be ranges (e.g., "1:3") which expandRange() converts to arrays
52
- let sourceUids = section[2] && typeof section[2].value === 'string' ? expandRange(section[2].value) : false;
53
- let destinationUids = section[3] && typeof section[3].value === 'string' ? expandRange(section[3].value) : false;
54
- // Build a source->destination UID map for the caller to track where messages went
55
- if (sourceUids && destinationUids && sourceUids.length === destinationUids.length) {
56
- map.uidMap = new Map(sourceUids.map((uid, i) => [uid, destinationUids[i]]));
57
- }
58
- }
59
- break;
60
- }
39
+ parseCopyUid(response.response, map);
61
40
 
62
41
  return map;
63
42
  } catch (err) {
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const { expandRange } = require('../tools.js');
4
+
5
+ /**
6
+ * Parses COPYUID response code from an IMAP response (RFC 4315).
7
+ * Used by both COPY and MOVE commands to extract the UID mapping
8
+ * from source mailbox to destination mailbox.
9
+ *
10
+ * @param {Object} response - IMAP response object with attributes
11
+ * @param {Object} map - Result map to populate with uidValidity and uidMap
12
+ */
13
+ function parseCopyUid(response, map) {
14
+ let section = response.attributes && response.attributes[0] && response.attributes[0].section;
15
+ let responseCode = section && section.length && section[0] && typeof section[0].value === 'string' ? section[0].value : '';
16
+
17
+ if (responseCode !== 'COPYUID') {
18
+ return;
19
+ }
20
+
21
+ let uidValidity = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
22
+ if (uidValidity !== false) {
23
+ map.uidValidity = uidValidity;
24
+ }
25
+
26
+ let sourceUids = section[2] && typeof section[2].value === 'string' ? expandRange(section[2].value) : false;
27
+ let destinationUids = section[3] && typeof section[3].value === 'string' ? expandRange(section[3].value) : false;
28
+ if (sourceUids && destinationUids && sourceUids.length === destinationUids.length) {
29
+ map.uidMap = new Map(sourceUids.map((uid, i) => [uid, destinationUids[i]]));
30
+ }
31
+ }
32
+
33
+ module.exports = { parseCopyUid };
@@ -90,9 +90,7 @@ async function runIdle(connection) {
90
90
  }
91
91
  }
92
92
  },
93
- onSend: () => {
94
- //idleSent = true;
95
- }
93
+ onSend: () => {}
96
94
  });
97
95
 
98
96
  // Clean up: unset preCheck and resolve any remaining waiters before processing the response.
@@ -206,10 +204,10 @@ module.exports = async (connection, maxIdleTime) => {
206
204
 
207
205
  case 'STATUS':
208
206
  {
209
- let statusArgs = [selectCommand.arguments[0], []]; // path
210
- for (let key of ['MESSAGES', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN']) {
211
- statusArgs[1].push({ type: 'ATOM', value: key.toUpperCase() });
212
- }
207
+ let statusArgs = [
208
+ selectCommand.arguments[0],
209
+ ['MESSAGES', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN'].map(key => ({ type: 'ATOM', value: key }))
210
+ ];
213
211
  connection.log.debug({ src: 'c', msg: `Running STATUS to detect changes in folder` });
214
212
  response = await connection.exec('STATUS', statusArgs);
215
213
  }
@@ -43,7 +43,7 @@ module.exports = async (connection, reference, mailbox, options) => {
43
43
 
44
44
  // Build the list of STATUS data items to request (MESSAGES, UIDNEXT, etc.)
45
45
  if (options.statusQuery) {
46
- Object.keys(options.statusQuery || {}).forEach(key => {
46
+ Object.keys(options.statusQuery).forEach(key => {
47
47
  if (!options.statusQuery[key]) {
48
48
  return;
49
49
  }
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { normalizePath, encodePath, expandRange, enhanceCommandError } = require('../tools.js');
3
+ const { normalizePath, encodePath, enhanceCommandError } = require('../tools.js');
4
+ const { parseCopyUid } = require('./copyuid-parser.js');
4
5
 
5
6
  /**
6
7
  * Moves messages from the current mailbox to another mailbox.
@@ -36,30 +37,6 @@ module.exports = async (connection, range, destination, options) => {
36
37
  return result;
37
38
  }
38
39
 
39
- // Extract COPYUID response code (UIDPLUS, RFC 4315) from either an untagged
40
- // OK response or the final tagged OK. MOVE uses the same COPYUID format as COPY
41
- // to report the source-to-destination UID mapping.
42
- let checkMoveInfo = response => {
43
- let section = response.attributes && response.attributes[0] && response.attributes[0].section;
44
- let responseCode = section && section.length && section[0] && typeof section[0].value === 'string' ? section[0].value : '';
45
- switch (responseCode) {
46
- case 'COPYUID':
47
- {
48
- let uidValidity = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
49
- if (uidValidity) {
50
- map.uidValidity = uidValidity;
51
- }
52
-
53
- let sourceUids = section[2] && typeof section[2].value === 'string' ? expandRange(section[2].value) : false;
54
- let destinationUids = section[3] && typeof section[3].value === 'string' ? expandRange(section[3].value) : false;
55
- if (sourceUids && destinationUids && sourceUids.length === destinationUids.length) {
56
- map.uidMap = new Map(sourceUids.map((uid, i) => [uid, destinationUids[i]]));
57
- }
58
- }
59
- break;
60
- }
61
- };
62
-
63
40
  let response;
64
41
  try {
65
42
  // Some servers send COPYUID in an untagged OK before the tagged response,
@@ -67,13 +44,13 @@ module.exports = async (connection, range, destination, options) => {
67
44
  response = await connection.exec(options.uid ? 'UID MOVE' : 'MOVE', attributes, {
68
45
  untagged: {
69
46
  OK: async untagged => {
70
- checkMoveInfo(untagged);
47
+ parseCopyUid(untagged, map);
71
48
  }
72
49
  }
73
50
  });
74
51
  response.next();
75
52
 
76
- checkMoveInfo(response.response);
53
+ parseCopyUid(response.response, map);
77
54
  return map;
78
55
  } catch (err) {
79
56
  await enhanceCommandError(err);
@@ -28,9 +28,7 @@ module.exports = async (connection, range, flags, options) => {
28
28
  // [+|-]FLAGS[.SILENT] or [+|-]X-GM-LABELS
29
29
  // Where: no prefix = replace all, + = add, - = remove
30
30
  // .SILENT suppresses the server from sending back updated flags (saves bandwidth).
31
- let operation;
32
-
33
- operation = 'FLAGS';
31
+ let operation = 'FLAGS';
34
32
 
35
33
  if (options.useLabels) {
36
34
  // Gmail labels (X-GM-EXT-1 extension): operates on labels instead of IMAP flags
@@ -71,9 +71,7 @@ module.exports = async (response, options) => {
71
71
  // - This is the first token (resp is empty)
72
72
  // - This is a sub-array element in a consecutive-list context (no space between adjacent lists)
73
73
  if (lastType === 'LITERAL' || (!['(', '<', '['].includes(lastRespByte) && resp.length)) {
74
- if (options.subArray) {
75
- // ignore separator between consecutive sub-arrays in a list
76
- } else {
74
+ if (!options.subArray) {
77
75
  resp.push(formatRespEntry(' '));
78
76
  }
79
77
  }
@@ -39,13 +39,7 @@ function expandRange(start, end) {
39
39
  * @returns {string} The source string with excluded characters removed.
40
40
  */
41
41
  function excludeChars(source, exclude) {
42
- let sourceArr = Array.prototype.slice.call(source);
43
- for (let i = sourceArr.length - 1; i >= 0; i--) {
44
- if (exclude.indexOf(sourceArr[i]) >= 0) {
45
- sourceArr.splice(i, 1);
46
- }
47
- }
48
- return sourceArr.join('');
42
+ return Array.prototype.filter.call(source, ch => exclude.indexOf(ch) < 0).join('');
49
43
  }
50
44
 
51
45
  module.exports = {
@@ -61,7 +61,7 @@ module.exports = async (command, options) => {
61
61
  // "UID MOVE", "UID SEARCH", "UID EXPUNGE", and "AUTHENTICATE PLAIN", etc.
62
62
  // For these, the first word is consumed as the command, then we read the
63
63
  // subcommand and concatenate them (e.g., "UID" + " " + "FETCH" -> "UID FETCH").
64
- if (['UID', 'AUTHENTICATE'].indexOf((response.command || '').toUpperCase()) >= 0) {
64
+ if (['UID', 'AUTHENTICATE'].includes((response.command || '').toUpperCase())) {
65
65
  await parser.getSpace();
66
66
  response.command += ' ' + (await parser.getElement(imapFormalSyntax.command()));
67
67
  }
@@ -172,17 +172,13 @@ class ImapStream extends Transform {
172
172
  this.literals = [];
173
173
 
174
174
  if (payload.length) {
175
- // remove final line terminator
176
- let skipBytes = 0;
177
- if (payload.length >= 1 && payload[payload.length - 1] === LF) {
178
- skipBytes++;
179
- if (payload.length >= 2 && payload[payload.length - 2] === CR) {
180
- skipBytes++;
175
+ // remove final line terminator (\n or \r\n)
176
+ if (payload[payload.length - 1] === LF) {
177
+ let end = payload.length - 1;
178
+ if (end > 0 && payload[end - 1] === CR) {
179
+ end--;
181
180
  }
182
- }
183
-
184
- if (skipBytes) {
185
- payload = payload.slice(0, payload.length - skipBytes);
181
+ payload = payload.slice(0, end);
186
182
  }
187
183
 
188
184
  if (payload.length) {
@@ -278,9 +274,7 @@ class ImapStream extends Transform {
278
274
  // Queue the chunk for async processing. The 'next' callback serves as
279
275
  // backpressure: it is called only after this chunk is fully processed,
280
276
  // which signals the writable side that more data can be accepted.
281
- if (chunk && chunk.length) {
282
- this.inputQueue.push({ chunk, next });
283
- }
277
+ this.inputQueue.push({ chunk, next });
284
278
 
285
279
  if (!this.processingInput) {
286
280
  this.processingInput = true;
@@ -114,7 +114,7 @@ class ParserInstance {
114
114
  async getElement(syntax) {
115
115
  let match, element, errPos;
116
116
 
117
- if (this.remainder.match(/^\s/)) {
117
+ if (/^\s/.test(this.remainder)) {
118
118
  let error = new Error(`Unexpected whitespace at position ${this.pos} [E1]`);
119
119
  error.code = 'ParserError1';
120
120
  error.parserContext = { input: this.input, pos: this.pos };
@@ -209,7 +209,7 @@ class ParserInstance {
209
209
  throw error;
210
210
  }
211
211
 
212
- if (this.remainder.match(/^\s/)) {
212
+ if (/^\s/.test(this.remainder)) {
213
213
  let error = new Error(`Unexpected whitespace at position ${this.pos} [E7]`);
214
214
  error.code = 'ParserError7';
215
215
  error.parserContext = { input: this.input, element: this.remainder, pos: this.pos };
@@ -272,7 +272,7 @@ class TokenParser {
272
272
  case '~': {
273
273
  let nextChr = this.str.charAt(i + 1);
274
274
  if (nextChr !== '{') {
275
- if (imapFormalSyntax['ATOM-CHAR']().indexOf(nextChr) >= 0) {
275
+ if (imapFormalSyntax['ATOM-CHAR']().includes(nextChr)) {
276
276
  // '~' not followed by '{' but followed by an ATOM char: treat as ATOM
277
277
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
278
278
  this.currentNode.type = 'ATOM';
@@ -369,7 +369,7 @@ class TokenParser {
369
369
  // Allow \ as the first char for atom to support system flags
370
370
  // Allow % to support LIST '' %
371
371
  // Allow 8bit characters (presumably unicode)
372
- if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== '\\' && chr !== '%' && chr.charCodeAt(0) < 0x80) {
372
+ if (!imapFormalSyntax['ATOM-CHAR']().includes(chr) && chr !== '\\' && chr !== '%' && chr.charCodeAt(0) < 0x80) {
373
373
  let error = new Error(`Unexpected char at position ${this.pos + i} [E13: ${JSON.stringify(chr)}]`);
374
374
  error.code = 'ParserError13';
375
375
  error.parserContext = { input: this.str, pos: this.pos + i, chr };
@@ -422,7 +422,7 @@ class TokenParser {
422
422
 
423
423
  // [ starts a section group for this element
424
424
  // Allowed only for selected elements, otherwise falls through to regular ATOM processing
425
- if (chr === '[' && ['BODY', 'BODY.PEEK', 'BINARY', 'BINARY.PEEK'].indexOf(this.currentNode.value.toUpperCase()) >= 0) {
425
+ if (chr === '[' && ['BODY', 'BODY.PEEK', 'BINARY', 'BINARY.PEEK'].includes(this.currentNode.value.toUpperCase())) {
426
426
  this.currentNode.endPos = this.pos + i;
427
427
  this.currentNode = this.createNode(this.currentNode.parentNode, this.pos + i);
428
428
  this.currentNode.type = 'SECTION';
@@ -433,7 +433,7 @@ class TokenParser {
433
433
 
434
434
  // if the char is not ATOM compatible, throw. Allow \* as an exception
435
435
  if (
436
- imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 &&
436
+ !imapFormalSyntax['ATOM-CHAR']().includes(chr) &&
437
437
  chr.charCodeAt(0) < 0x80 && // allow 8bit (presumably unicode) bytes
438
438
  chr !== ']' &&
439
439
  !(chr === '*' && this.currentNode.value === '\\') &&
@@ -496,21 +496,21 @@ class TokenParser {
496
496
  break;
497
497
  }
498
498
 
499
- if (chr === '.' && (!this.currentNode.value.length || this.currentNode.value.match(/\./))) {
499
+ if (chr === '.' && (this.currentNode.value === '' || this.currentNode.value.includes('.'))) {
500
500
  let error = new Error(`Unexpected partial separator . at position ${this.pos + i} [E20]`);
501
501
  error.code = 'ParserError20';
502
502
  error.parserContext = { input: this.str, pos: this.pos + i, chr };
503
503
  throw error;
504
504
  }
505
505
 
506
- if (imapFormalSyntax.DIGIT().indexOf(chr) < 0 && chr !== '.') {
506
+ if (!imapFormalSyntax.DIGIT().includes(chr) && chr !== '.') {
507
507
  let error = new Error(`Unexpected char at position ${this.pos + i} [E21: ${JSON.stringify(chr)}]`);
508
508
  error.code = 'ParserError21';
509
509
  error.parserContext = { input: this.str, pos: this.pos + i, chr };
510
510
  throw error;
511
511
  }
512
512
 
513
- if (this.currentNode.value.match(/^0$|\.0$/) && chr !== '.') {
513
+ if ((this.currentNode.value === '0' || this.currentNode.value.endsWith('.0')) && chr !== '.') {
514
514
  let error = new Error(`Invalid partial at position ${this.pos + i} [E22: ${JSON.stringify(chr)}]`);
515
515
  error.code = 'ParserError22';
516
516
  error.parserContext = { input: this.str, pos: this.pos + i, chr };
@@ -606,7 +606,7 @@ class TokenParser {
606
606
  }
607
607
  break;
608
608
  }
609
- if (imapFormalSyntax.DIGIT().indexOf(chr) < 0) {
609
+ if (!imapFormalSyntax.DIGIT().includes(chr)) {
610
610
  let error = new Error(`Unexpected char at position ${this.pos + i} [E25: ${JSON.stringify(chr)}]`);
611
611
  error.code = 'ParserError25';
612
612
  error.parserContext = { input: this.str, pos: this.pos + i, chr };
@@ -664,7 +664,7 @@ class TokenParser {
664
664
  throw error;
665
665
  }
666
666
  } else if (chr === '*') {
667
- if ([',', ':'].indexOf(this.currentNode.value.at(-1)) < 0) {
667
+ if (![',', ':'].includes(this.currentNode.value.at(-1))) {
668
668
  let error = new Error(`Unexpected range wildcard at position ${this.pos + i} [E30]`);
669
669
  error.code = 'ParserError30';
670
670
  error.parserContext = { input: this.str, pos: this.pos + i, chr };