imapflow 1.0.168 → 1.0.170

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.170](https://github.com/postalsys/imapflow/compare/v1.0.169...v1.0.170) (2024-12-05)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **uidvalidity:** Do not expect UIDVALIDITY to always exist ([7761653](https://github.com/postalsys/imapflow/commit/7761653e92965a09be8a872813363eb81f7f3d9a))
9
+
10
+ ## [1.0.169](https://github.com/postalsys/imapflow/compare/v1.0.168...v1.0.169) (2024-11-08)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **fetc:** Treat 'Some of the requested messages no longer exist' as a success, not a failure ([565f988](https://github.com/postalsys/imapflow/commit/565f988178fc4a4d3989fe027451980f3ee0d412))
16
+
3
17
  ## [1.0.168](https://github.com/postalsys/imapflow/compare/v1.0.167...v1.0.168) (2024-11-07)
4
18
 
5
19
 
@@ -151,13 +151,12 @@ module.exports = async (connection, username, { accessToken, password }) => {
151
151
  }
152
152
 
153
153
  if (password) {
154
- if (connection.capabilities.has('AUTH=LOGIN')) {
155
- return await authLogin(connection, username, password);
156
- }
157
-
158
154
  if (connection.capabilities.has('AUTH=PLAIN')) {
159
155
  return await authPlain(connection, username, password);
160
156
  }
157
+ if (connection.capabilities.has('AUTH=LOGIN')) {
158
+ return await authLogin(connection, username, password);
159
+ }
161
160
  }
162
161
 
163
162
  throw new Error('Unsupported authentication mechanism');
@@ -40,7 +40,7 @@ module.exports = async (connection, path, options) => {
40
40
  extraArgs.push([
41
41
  { type: 'ATOM', value: 'QRESYNC' },
42
42
  [
43
- { type: 'ATOM', value: options.uidValidity.toString() },
43
+ { type: 'ATOM', value: options.uidValidity?.toString() },
44
44
  { type: 'ATOM', value: options.changedSince.toString() }
45
45
  ]
46
46
  ]);
package/lib/imap-flow.js CHANGED
@@ -119,8 +119,19 @@ class ImapFlow extends EventEmitter {
119
119
  * @param {Object} options IMAP connection options
120
120
  * @param {String} options.host Hostname of the IMAP server
121
121
  * @param {Number} options.port Port number for the IMAP server
122
- * @param {Boolean} [options.secure=false] Should the connection be established over TLS.
123
- * If `false` then connection is upgraded to TLS using STARTTLS extension before authentication
122
+ * @param {Boolean} [options.secure=false] Should the connection be established immediately and directly over TLS? Typically on port 993.
123
+ * @param {Boolean} [options.doSTARTTLS=undefined] Should the connection be established using STARTTLS?
124
+ * * If `true`, the connection is first established as unencrypted and then upgraded to TLS using STARTTLS, before authentication.
125
+ * If the server does not advertize the `STARTTLS` `CAPABILITY`, or the upgrade fails for other reasons, then the connection fails.
126
+ * Note: The combination `secure=true` (direct TLS) and `doSTARTTLS=true` is invalid.
127
+ * * If `false`, then STARTTLS will not be used, even if the server advertizes it in IMAP `CAPABILITY`.
128
+ * This helps with servers that have a broken TLS configuration.
129
+ * If `doSTARTTLS=false` and `secure=false`, then a plain unencrypted socket is used.
130
+ * Be sure to clearly warn the user about the consequences.
131
+ * * If `undefined` (default) and `secure=false` (default), the connection is upgraded using STARTTLS before authentication, /only if possible/ .
132
+ * If not possible, the connection will use an unencrypted plain socket.
133
+ * This can mean TLS is used under normal circumstances, but a serious attacker can force an unencrypted connection and steal passwords,
134
+ * called "downgrade attack". This can lead to a false sense of security. Be sure to warn the user.
124
135
  * @param {String} [options.servername] Servername for SNI (or when host is set to an IP address)
125
136
  * @param {Boolean} [options.disableCompression=false] if `true` then client does not try to use COMPRESS=DEFLATE extension
126
137
  * @param {Object} options.auth Authentication options. Authentication is requested automatically during <code>connect()</code>
@@ -571,9 +582,29 @@ class ImapFlow extends EventEmitter {
571
582
  let err = new Error('Command failed');
572
583
  err.response = parsed;
573
584
  err.responseStatus = parsed.command.toUpperCase();
585
+
586
+ try {
587
+ err.executedCommand =
588
+ parsed.tag +
589
+ (
590
+ await compiler(request, {
591
+ isLogging: true
592
+ })
593
+ ).toString();
594
+ } catch (err) {
595
+ // ignore
596
+ }
597
+
574
598
  if (txt) {
575
599
  err.responseText = txt;
576
600
 
601
+ if (err.responseStatus === 'NO' && txt.includes('Some of the requested messages no longer exist')) {
602
+ // Treat as successful response
603
+ this.log.warn({ msg: 'Partial FETCH response', cid: this.id, err });
604
+ await new Promise(resolve => request.resolve({ response: parsed, next: resolve }));
605
+ break;
606
+ }
607
+
577
608
  let throttleDelay = false;
578
609
 
579
610
  // MS365 throttling
@@ -596,7 +627,7 @@ class ImapFlow extends EventEmitter {
596
627
  delayResponse = 5 * 60 * 1000;
597
628
  }
598
629
 
599
- this.log.warn({ msg: 'Throttling detected', err, cid: this.id, throttleDelay, delayResponse });
630
+ this.log.warn({ msg: 'Throttling detected', cid: this.id, throttleDelay, delayResponse, err });
600
631
  await new Promise(r => setTimeout(r, delayResponse));
601
632
  }
602
633
  }
@@ -695,10 +726,7 @@ class ImapFlow extends EventEmitter {
695
726
  this.idRequested = await this.run('ID', this.clientInfo);
696
727
  }
697
728
 
698
- // try to use STARTTLS is possible
699
- if (!this.secureConnection) {
700
- await this.upgradeConnection();
701
- }
729
+ await this.upgradeToSTARTTLS();
702
730
 
703
731
  let authenticated = await this.authenticate();
704
732
  if (!authenticated) {
@@ -817,21 +845,47 @@ class ImapFlow extends EventEmitter {
817
845
  });
818
846
  }
819
847
 
820
- async upgradeConnection() {
848
+ _failSTARTTLS() {
849
+ if (this.options.doSTARTTLS === true) {
850
+ // STARTTLS configured as requirement
851
+ let err = new Error('Server does not support STARTTLS');
852
+ err.tlsFailed = true;
853
+ throw err;
854
+ } else {
855
+ // Opportunistic STARTTLS. But it's not possible right now.
856
+ // Attention: Could be a downgrade attack.
857
+ return false;
858
+ }
859
+ }
860
+
861
+ /**
862
+ * Tries to upgrade the connection to TLS using STARTTLS.
863
+ * @throws if STARTTLS is required, but not possible.
864
+ * @returns {boolean} true, if the connection is now protected by TLS, either direct TLS or STARTTLS.
865
+ */
866
+ async upgradeToSTARTTLS() {
867
+ if (this.options.doSTARTTLS === true && this.options.secure === true) {
868
+ throw new Error('Misconfiguration: Cannot set both secure=true for TLS and doSTARTTLS=true for STARTTLS.');
869
+ }
870
+
821
871
  if (this.secureConnection) {
822
- // already secure
872
+ // Already using direct TLS. No need for STARTTLS.
823
873
  return true;
824
874
  }
825
875
 
826
- if (!this.capabilities.has('STARTTLS')) {
827
- // can not upgrade
876
+ if (this.options.doSTARTTLS === false) {
877
+ // STARTTLS explictly disabled by config
828
878
  return false;
829
879
  }
830
880
 
881
+ if (!this.capabilities.has('STARTTLS')) {
882
+ return this._failSTARTTLS();
883
+ }
884
+
831
885
  this.expectCapabilityUpdate = true;
832
886
  let canUpgrade = await this.run('STARTTLS');
833
887
  if (!canUpgrade) {
834
- return;
888
+ return this._failSTARTTLS();
835
889
  }
836
890
 
837
891
  this.socket.unpipe(this.streamer);
@@ -855,6 +909,7 @@ class ImapFlow extends EventEmitter {
855
909
  }
856
910
  setImmediate(() => this.close());
857
911
  this.upgrading = false;
912
+ err.tlsFailed = true;
858
913
  reject(err);
859
914
  });
860
915
 
@@ -864,6 +919,7 @@ class ImapFlow extends EventEmitter {
864
919
  }
865
920
  setImmediate(() => this.close());
866
921
  let err = new Error('Failed to upgrade connection in required time');
922
+ err.tlsFailed = true;
867
923
  err.code = 'UPGRADE_TIMEOUT';
868
924
  reject(err);
869
925
  }, UPGRADE_TIMEOUT);
package/lib/tools.js CHANGED
@@ -390,7 +390,11 @@ module.exports = {
390
390
  }
391
391
  }
392
392
 
393
- map.id = map.emailId || createHash('md5').update([path, mailbox.uidValidity.toString(), map.uid.toString()].join(':')).digest('hex');
393
+ map.id =
394
+ map.emailId ||
395
+ createHash('md5')
396
+ .update([path, mailbox.uidValidity?.toString() || '', map.uid.toString()].join(':'))
397
+ .digest('hex');
394
398
  }
395
399
 
396
400
  if (map.flags) {
package/lib/types.d.ts CHANGED
@@ -8,8 +8,19 @@ declare module "imapflow" {
8
8
  * @param options - IMAP connection options
9
9
  * @param options.host - Hostname of the IMAP server
10
10
  * @param options.port - Port number for the IMAP server
11
- * @param [options.secure = false] - Should the connection be established over TLS.
12
- * If `false` then connection is upgraded to TLS using STARTTLS extension before authentication
11
+ * @param [options.secure = false] - Should the connection be established immediately and directly over TLS? Typically on port 993.
12
+ * @param [options.doSTARTTLS] - Should the connection be established using STARTTLS?
13
+ * * If `true`, the connection is first established as unencrypted and then upgraded to TLS using STARTTLS, before authentication.
14
+ * If the server does not advertize the `STARTTLS` `CAPABILITY`, or the upgrade fails for other reasons, then the connection fails.
15
+ * Note: The combination `secure=true` (direct TLS) and `doSTARTTLS=true` is invalid.
16
+ * * If `false`, then STARTTLS will not be used, even if the server advertizes it in IMAP `CAPABILITY`.
17
+ * This helps with servers that have a broken TLS configuration.
18
+ * If `doSTARTTLS=false` and `secure=false`, then a plain unencrypted socket is used.
19
+ * Be sure to clearly warn the user about the consequences.
20
+ * * If `undefined` (default) and `secure=false` (default), the connection is upgraded using STARTTLS before authentication, /only if possible/ .
21
+ * If not possible, the connection will use an unencrypted plain socket.
22
+ * This can mean TLS is used under normal circumstances, but a serious attacker can force an unencrypted connection and steal passwords,
23
+ * called "downgrade attack". This can lead to a false sense of security. Be sure to warn the user.
13
24
  * @param [options.servername] - Servername for SNI (or when host is set to an IP address)
14
25
  * @param [options.disableCompression = false] - if `true` then client does not try to use COMPRESS=DEFLATE extension
15
26
  * @param options.auth - Authentication options. Authentication is requested automatically during <code>connect()</code>
@@ -41,6 +52,7 @@ declare module "imapflow" {
41
52
  host: string;
42
53
  port: number;
43
54
  secure?: boolean;
55
+ doSTARTTLS?: boolean;
44
56
  servername?: string;
45
57
  disableCompression?: boolean;
46
58
  auth: {
@@ -122,6 +134,11 @@ declare module "imapflow" {
122
134
  * If `true` then in addition of sending data to logger, ImapFlow emits 'log' events with the same data
123
135
  */
124
136
  emitLogs: boolean;
137
+ /**
138
+ * Tries to upgrade the connection to TLS using STARTTLS.
139
+ * @returns true, if the connection is now protected by TLS, either direct TLS or STARTTLS.
140
+ */
141
+ upgradeToSTARTTLS(): boolean;
125
142
  /**
126
143
  * Initiates a connection against IMAP server. Throws if anything goes wrong. This is something you have to call before you can run any IMAP commands
127
144
  * @example
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.168",
3
+ "version": "1.0.170",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "./lib/imap-flow.js",
6
6
  "scripts": {
@@ -32,7 +32,7 @@
32
32
  "@babel/eslint-plugin": "7.25.9",
33
33
  "@babel/plugin-syntax-class-properties": "7.12.13",
34
34
  "@babel/preset-env": "7.26.0",
35
- "@types/node": "22.9.0",
35
+ "@types/node": "22.10.1",
36
36
  "eslint": "8.57.0",
37
37
  "eslint-config-nodemailer": "1.2.0",
38
38
  "eslint-config-prettier": "9.1.0",
@@ -40,7 +40,7 @@
40
40
  "grunt-cli": "1.5.0",
41
41
  "grunt-contrib-nodeunit": "5.0.0",
42
42
  "grunt-eslint": "24.3.0",
43
- "imapflow-jsdoc-template": "3.4.0-imapflow.1",
43
+ "imapflow-jsdoc-template": "3.4.0-imapflow.2",
44
44
  "jsdoc": "3.6.11",
45
45
  "st": "3.0.1",
46
46
  "tsd-jsdoc": "2.5.0"
@@ -49,9 +49,9 @@
49
49
  "encoding-japanese": "2.2.0",
50
50
  "iconv-lite": "0.6.3",
51
51
  "libbase64": "1.3.0",
52
- "libmime": "5.3.5",
53
- "libqp": "2.1.0",
54
- "mailsplit": "5.4.0",
52
+ "libmime": "5.3.6",
53
+ "libqp": "2.1.1",
54
+ "mailsplit": "5.4.2",
55
55
  "nodemailer": "6.9.16",
56
56
  "pino": "9.5.0",
57
57
  "socks": "2.8.3"