imapflow 1.0.147 → 1.0.149

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.
@@ -0,0 +1,24 @@
1
+ name: 'Close stale issues and PRs'
2
+ on:
3
+ schedule:
4
+ - cron: '30 1 * * *'
5
+
6
+ jobs:
7
+ stale:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/stale@v8
11
+ with:
12
+ stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
13
+ stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
14
+ close-issue-message: 'This issue was closed because it has been stalled for 15 days with no activity.'
15
+ close-pr-message: 'This PR was closed because it has been stalled for 20 days with no activity.'
16
+ days-before-issue-stale: 30
17
+ days-before-pr-stale: 45
18
+ days-before-issue-close: 15
19
+ days-before-pr-close: 20
20
+ stale-issue-label: 'no-issue-activity'
21
+ exempt-issue-labels: 'pending,work-in-progress'
22
+ stale-pr-label: 'no-pr-activity'
23
+ exempt-pr-labels: 'awaiting-approval,work-in-progress'
24
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.149](https://github.com/postalsys/imapflow/compare/v1.0.148...v1.0.149) (2024-01-31)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **flag-colors:** added a method to set Flag Color. The color is also included in the Fetch response structure ([d840951](https://github.com/postalsys/imapflow/commit/d8409513075b864e9131665af3f1670a0714d05c))
9
+
10
+ ## [1.0.148](https://github.com/postalsys/imapflow/compare/v1.0.147...v1.0.148) (2024-01-16)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **logout:** Fixed race condition for logout and TCP close. Fixes [#161](https://github.com/postalsys/imapflow/issues/161) ([39a7333](https://github.com/postalsys/imapflow/commit/39a7333a39499e4876abb29149f7c72f2f99a63d))
16
+
3
17
  ## [1.0.147](https://github.com/postalsys/imapflow/compare/v1.0.146...v1.0.147) (2023-10-26)
4
18
 
5
19
 
package/jsdoc.json CHANGED
@@ -23,6 +23,6 @@
23
23
  "encoding": "utf8",
24
24
  "private": true,
25
25
  "recurse": true,
26
- "template": "./node_modules/braintree-jsdoc-template"
26
+ "template": "./node_modules/imapflow-jsdoc-template"
27
27
  }
28
28
  }
@@ -18,6 +18,9 @@ module.exports = async connection => {
18
18
  response = await connection.exec('LOGOUT');
19
19
  return true;
20
20
  } catch (err) {
21
+ if (err.code === 'NoConnection') {
22
+ return true;
23
+ }
21
24
  connection.log.warn({ err, cid: connection.id });
22
25
  return false;
23
26
  } finally {
package/lib/imap-flow.js CHANGED
@@ -29,7 +29,17 @@ const { PassThrough } = require('stream');
29
29
 
30
30
  const { proxyConnection } = require('./proxy-connection');
31
31
 
32
- const { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, getDecoder, packMessageRange, normalizePath, expandRange } = require('./tools');
32
+ const {
33
+ comparePaths,
34
+ updateCapabilities,
35
+ getFolderTree,
36
+ formatMessageResponse,
37
+ getDecoder,
38
+ packMessageRange,
39
+ normalizePath,
40
+ expandRange,
41
+ getColorFlags
42
+ } = require('./tools');
33
43
 
34
44
  const imapCommands = require('./imap-commands.js');
35
45
 
@@ -371,7 +381,9 @@ class ImapFlow extends EventEmitter {
371
381
  let request = this.requestTagMap.get(data.tag);
372
382
  if (request) {
373
383
  this.requestTagMap.delete(request.tag);
374
- request.reject(new Error('Connection not available'));
384
+ const error = new Error('Connection not available');
385
+ error.code = 'NoConnection';
386
+ request.reject(error);
375
387
  }
376
388
  }
377
389
  return;
@@ -595,6 +607,7 @@ class ImapFlow extends EventEmitter {
595
607
 
596
608
  default: {
597
609
  let err = new Error('Invalid server response');
610
+ err.code = 'InvalidResponse';
598
611
  err.response = parsed;
599
612
  request.reject(err);
600
613
  break;
@@ -1174,6 +1187,10 @@ class ImapFlow extends EventEmitter {
1174
1187
 
1175
1188
  updateEvent.flags = message.flags;
1176
1189
 
1190
+ if (message.flagColor) {
1191
+ updateEvent.flagColor = message.flagColor;
1192
+ }
1193
+
1177
1194
  this.emit('flags', updateEvent);
1178
1195
  }
1179
1196
  }
@@ -1393,7 +1410,7 @@ class ImapFlow extends EventEmitter {
1393
1410
  * await client.logout();
1394
1411
  */
1395
1412
  async logout() {
1396
- await this.run('LOGOUT');
1413
+ return await this.run('LOGOUT');
1397
1414
  }
1398
1415
 
1399
1416
  /**
@@ -1432,9 +1449,11 @@ class ImapFlow extends EventEmitter {
1432
1449
  // reject command that is currently processed
1433
1450
  if (this.currentRequest && this.requestTagMap.has(this.currentRequest.tag)) {
1434
1451
  let request = this.requestTagMap.get(this.currentRequest.tag);
1435
- if (request) {
1452
+ if (request && ['LOGOUT'].includes(request.command)) {
1436
1453
  this.requestTagMap.delete(request.tag);
1437
- request.reject(new Error('Connection not available'));
1454
+ const error = new Error('Connection not available');
1455
+ error.code = 'NoConnection';
1456
+ request.reject(error);
1438
1457
  }
1439
1458
  this.currentRequest = false;
1440
1459
  }
@@ -1446,7 +1465,9 @@ class ImapFlow extends EventEmitter {
1446
1465
  let request = this.requestTagMap.get(req.tag);
1447
1466
  if (request) {
1448
1467
  this.requestTagMap.delete(request.tag);
1449
- request.reject(new Error('Connection not available'));
1468
+ const error = new Error('Connection not available');
1469
+ error.code = 'NoConnection';
1470
+ request.reject(error);
1450
1471
  }
1451
1472
  }
1452
1473
  }
@@ -1919,6 +1940,68 @@ class ImapFlow extends EventEmitter {
1919
1940
  return await this.run('STORE', range, flags, queryOpts);
1920
1941
  }
1921
1942
 
1943
+ /**
1944
+ * Sets a colored flag for an email. Only supported by mail clients like Apple Mail
1945
+ *
1946
+ * @param {SequenceString | Number[] | SearchObject} range Range to filter the messages
1947
+ * @param {string} The color to set. One of 'red', 'orange', 'yellow', 'green', 'blue', 'purple', and 'grey'
1948
+ * @param {Object} [options]
1949
+ * @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
1950
+ * @param {BigInt} [options.unchangedSince] If set then only messages with a lower or equal `modseq` value are updated. Ignored if server does not support `CONDSTORE` extension.
1951
+ * @returns {Promise<Boolean>} Did the operation succeed or not
1952
+ *
1953
+ * @example
1954
+ * let mailbox = await client.mailboxOpen('INBOX');
1955
+ * // add a purple flag for all emails
1956
+ * await client.setFlagColor('1:*', 'Purple');
1957
+ */
1958
+ async setFlagColor(range, color, options) {
1959
+ options = options || {};
1960
+
1961
+ range = await this.resolveRange(range, options);
1962
+ if (!range) {
1963
+ return false;
1964
+ }
1965
+
1966
+ let flagChanges = getColorFlags(color);
1967
+ if (!flagChanges) {
1968
+ return false;
1969
+ }
1970
+
1971
+ let addResults;
1972
+ let removeResults;
1973
+
1974
+ if (flagChanges.add && flagChanges.add.length) {
1975
+ let queryOpts = Object.assign(
1976
+ {
1977
+ operation: 'add'
1978
+ },
1979
+ options,
1980
+ {
1981
+ useLabels: false, // override if set
1982
+ // prevent triggering a premature Flags change notification
1983
+ silent: flagChanges.remove && flagChanges.remove.length
1984
+ }
1985
+ );
1986
+
1987
+ addResults = await this.run('STORE', range, flagChanges.add, queryOpts);
1988
+ }
1989
+
1990
+ if (flagChanges.remove && flagChanges.remove.length) {
1991
+ let queryOpts = Object.assign(
1992
+ {
1993
+ operation: 'remove'
1994
+ },
1995
+ options,
1996
+ { useLabels: false } // override if set
1997
+ );
1998
+
1999
+ removeResults = await this.run('STORE', range, flagChanges.remove, queryOpts);
2000
+ }
2001
+
2002
+ return addResults || removeResults || false;
2003
+ }
2004
+
1922
2005
  /**
1923
2006
  * Delete messages from currently opened mailbox. Method does not indicate info about deleted messages,
1924
2007
  * instead you should be using {@link ImapFlow#expunge} event for this
@@ -2070,7 +2153,7 @@ class ImapFlow extends EventEmitter {
2070
2153
  * @typedef {Object} FetchQueryObject
2071
2154
  * @global
2072
2155
  * @property {Boolean} [uid] if `true` then include UID in the response
2073
- * @property {Boolean} [flags] if `true` then include flags Set in the response
2156
+ * @property {Boolean} [flags] if `true` then include flags Set in the response. Also adds `flagColor` to the response if the message is flagged.
2074
2157
  * @property {Boolean} [bodyStructure] if `true` then include parsed BODYSTRUCTURE object in the response
2075
2158
  * @property {Boolean} [envelope] if `true` then include parsed ENVELOPE object in the response
2076
2159
  * @property {Boolean} [internalDate] if `true` then include internal date value in the response
@@ -2141,6 +2224,7 @@ class ImapFlow extends EventEmitter {
2141
2224
  * @property {Set<string>} [labels] a Set of labels. Only present if server supports `X-GM-EXT-1` extension
2142
2225
  * @property {Number} [size] message size
2143
2226
  * @property {Set<string>} [flags] a set of message flags
2227
+ * @property {String} [flagColor] flag color like "red", or "yellow". This value is derived from the `flags` Set and it uses the same color rules as Apple Mail
2144
2228
  * @property {MessageEnvelopeObject} [envelope] message envelope
2145
2229
  * @property {MessageStructureObject} [bodyStructure] message body structure
2146
2230
  * @property {Date} [internalDate] message internal date
@@ -2602,7 +2686,7 @@ class ImapFlow extends EventEmitter {
2602
2686
  * @example
2603
2687
  * let mailbox = await client.mailboxOpen('INBOX');
2604
2688
  * // download body parts '2', and '3' from all messages in the selected mailbox
2605
- * let response = await client.downloadMany('*', ['2', '3]);
2689
+ * let response = await client.downloadMany('*', ['2', '3']);
2606
2690
  * process.stdout.write(response[2].content)
2607
2691
  * process.stdout.write(response[3].content)
2608
2692
  */
@@ -2728,8 +2812,9 @@ class ImapFlow extends EventEmitter {
2728
2812
  }
2729
2813
 
2730
2814
  if (this.socket.destroyed) {
2731
- let err = new Error('Connection not available');
2732
- throw err;
2815
+ const error = new Error('Connection not available');
2816
+ error.code = 'NoConnection';
2817
+ throw error;
2733
2818
  }
2734
2819
 
2735
2820
  clearTimeout(this.idleStartTimer);
@@ -2771,10 +2856,11 @@ class ImapFlow extends EventEmitter {
2771
2856
  const { resolve, reject, path, options, lockId } = this.locks.shift();
2772
2857
 
2773
2858
  if (!this.usable || this.socket.destroyed) {
2774
- // reject all
2775
- let err = new Error('Connection not available');
2776
2859
  this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId });
2777
- reject(err);
2860
+ // reject all
2861
+ let error = new Error('Connection not available');
2862
+ error.code = 'NoConnection';
2863
+ reject(error);
2778
2864
  return await this.processLocks(true);
2779
2865
  }
2780
2866
 
package/lib/tools.js CHANGED
@@ -8,6 +8,8 @@ const { createHash } = require('crypto');
8
8
  const { JPDecoder } = require('./jp-decoder');
9
9
  const iconv = require('iconv-lite');
10
10
 
11
+ const FLAG_COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];
12
+
11
13
  module.exports = {
12
14
  encodePath(connection, path) {
13
15
  path = (path || '').toString();
@@ -191,6 +193,43 @@ module.exports = {
191
193
  return tree;
192
194
  },
193
195
 
196
+ getFlagColor(flags) {
197
+ if (!flags.has('\\Flagged')) {
198
+ return null;
199
+ }
200
+
201
+ const bit0 = flags.has('$MailFlagBit0') ? 1 : 0;
202
+ const bit1 = flags.has('$MailFlagBit1') ? 2 : 0;
203
+ const bit2 = flags.has('$MailFlagBit2') ? 4 : 0;
204
+
205
+ const color = bit0 | bit1 | bit2; // eslint-disable-line no-bitwise
206
+
207
+ return FLAG_COLORS[color] || 'red'; // default to red for the unused \b111
208
+ },
209
+
210
+ getColorFlags(color) {
211
+ const colorCode = color ? FLAG_COLORS.indexOf((color || '').toString().toLowerCase().trim()) : null;
212
+ if (colorCode < 0 && colorCode !== null) {
213
+ return null;
214
+ }
215
+
216
+ const bits = [];
217
+ bits[0] = colorCode & 1; // eslint-disable-line no-bitwise
218
+ bits[1] = colorCode & 2; // eslint-disable-line no-bitwise
219
+ bits[2] = colorCode & 4; // eslint-disable-line no-bitwise
220
+
221
+ let result = { add: colorCode ? ['\\Flagged'] : [], remove: colorCode ? [] : ['\\Flagged'] };
222
+
223
+ for (let i = 0; i < bits.length; i++) {
224
+ if (bits[i]) {
225
+ result.add.push(`$MailFlagBit${i}`);
226
+ } else {
227
+ result.remove.push(`$MailFlagBit${i}`);
228
+ }
229
+ }
230
+ return result;
231
+ },
232
+
194
233
  async formatMessageResponse(untagged, mailbox) {
195
234
  let map = {};
196
235
 
@@ -351,6 +390,13 @@ module.exports = {
351
390
  map.id = map.emailId || createHash('md5').update([path, mailbox.uidValidity.toString(), map.uid.toString()].join(':')).digest('hex');
352
391
  }
353
392
 
393
+ if (map.flags) {
394
+ let flagColor = module.exports.getFlagColor(map.flags);
395
+ if (flagColor) {
396
+ map.flagColor = flagColor;
397
+ }
398
+ }
399
+
354
400
  return map;
355
401
  },
356
402
 
package/lib/types.d.ts CHANGED
@@ -364,6 +364,22 @@ declare module "imapflow" {
364
364
  unchangedSince?: bigint;
365
365
  useLabels?: boolean;
366
366
  }): Promise<Boolean>;
367
+ /**
368
+ * Sets a colored flag for an email. Only supported by mail clients like Apple Mail
369
+ * @example
370
+ * let mailbox = await client.mailboxOpen('INBOX');
371
+ * // add a purple flag for all emails
372
+ * await client.setFlagColor('1:*', 'Purple');
373
+ * @param range - Range to filter the messages
374
+ * @param The - color to set. One of 'red', 'orange', 'yellow', 'green', 'blue', 'purple', and 'grey'
375
+ * @param [options.uid] - If `true` then uses UID {@link SequenceString} instead of sequence numbers
376
+ * @param [options.unchangedSince] - If set then only messages with a lower or equal `modseq` value are updated. Ignored if server does not support `CONDSTORE` extension.
377
+ * @returns Did the operation succeed or not
378
+ */
379
+ setFlagColor(range: SequenceString | Number[] | SearchObject, The: string, options?: {
380
+ uid?: boolean;
381
+ unchangedSince?: bigint;
382
+ }): Promise<Boolean>;
367
383
  /**
368
384
  * Delete messages from currently opened mailbox. Method does not indicate info about deleted messages,
369
385
  * instead you should be using {@link ImapFlow#expunge} event for this
@@ -502,7 +518,7 @@ declare module "imapflow" {
502
518
  * @example
503
519
  * let mailbox = await client.mailboxOpen('INBOX');
504
520
  * // download body parts '2', and '3' from all messages in the selected mailbox
505
- * let response = await client.downloadMany('*', ['2', '3]);
521
+ * let response = await client.downloadMany('*', ['2', '3']);
506
522
  * process.stdout.write(response[2].content)
507
523
  * process.stdout.write(response[3].content)
508
524
  * @param range - UID or sequence number for the message to fetch
@@ -832,7 +848,7 @@ declare type CopyResponseObject = {
832
848
 
833
849
  /**
834
850
  * @property [uid] - if `true` then include UID in the response
835
- * @property [flags] - if `true` then include flags Set in the response
851
+ * @property [flags] - if `true` then include flags Set in the response. Also adds `flagColor` to the response if the message is flagged.
836
852
  * @property [bodyStructure] - if `true` then include parsed BODYSTRUCTURE object in the response
837
853
  * @property [envelope] - if `true` then include parsed ENVELOPE object in the response
838
854
  * @property [internalDate] - if `true` then include internal date value in the response
@@ -935,6 +951,7 @@ declare type MessageStructureObject = {
935
951
  * @property [labels] - a Set of labels. Only present if server supports `X-GM-EXT-1` extension
936
952
  * @property [size] - message size
937
953
  * @property [flags] - a set of message flags
954
+ * @property [flagColor] - flag color like "red", or "yellow". This value is derived from the `flags` Set and it uses the same color rules as Apple Mail
938
955
  * @property [envelope] - message envelope
939
956
  * @property [bodyStructure] - message body structure
940
957
  * @property [internalDate] - message internal date
@@ -951,6 +968,7 @@ declare type FetchMessageObject = {
951
968
  labels?: Set<string>;
952
969
  size?: number;
953
970
  flags?: Set<string>;
971
+ flagColor?: string;
954
972
  envelope?: MessageEnvelopeObject;
955
973
  bodyStructure?: MessageStructureObject;
956
974
  internalDate?: Date;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.147",
3
+ "version": "1.0.149",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "./lib/imap-flow.js",
6
6
  "scripts": {
@@ -28,19 +28,19 @@
28
28
  },
29
29
  "homepage": "https://imapflow.com/",
30
30
  "devDependencies": {
31
- "@babel/eslint-parser": "7.22.15",
32
- "@babel/eslint-plugin": "7.22.10",
31
+ "@babel/eslint-parser": "7.23.9",
32
+ "@babel/eslint-plugin": "7.23.5",
33
33
  "@babel/plugin-syntax-class-properties": "7.12.13",
34
- "@babel/preset-env": "7.23.2",
35
- "@types/node": "20.8.9",
36
- "braintree-jsdoc-template": "3.3.0",
37
- "eslint": "8.52.0",
34
+ "@babel/preset-env": "7.23.9",
35
+ "@types/node": "20.11.13",
36
+ "eslint": "8.56.0",
38
37
  "eslint-config-nodemailer": "1.2.0",
39
- "eslint-config-prettier": "9.0.0",
38
+ "eslint-config-prettier": "9.1.0",
40
39
  "grunt": "1.6.1",
41
40
  "grunt-cli": "1.4.3",
42
41
  "grunt-contrib-nodeunit": "5.0.0",
43
42
  "grunt-eslint": "24.3.0",
43
+ "imapflow-jsdoc-template": "3.4.0-imapflow.1",
44
44
  "jsdoc": "3.6.11",
45
45
  "st": "3.0.0",
46
46
  "tsd-jsdoc": "2.5.0"
@@ -52,8 +52,8 @@
52
52
  "libmime": "5.2.1",
53
53
  "libqp": "2.0.1",
54
54
  "mailsplit": "5.4.0",
55
- "nodemailer": "6.9.7",
56
- "pino": "8.16.1",
55
+ "nodemailer": "6.9.8",
56
+ "pino": "8.17.2",
57
57
  "socks": "2.7.1"
58
58
  }
59
59
  }