imapflow 2.0.1 → 2.0.3

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.3](https://github.com/postalsys/imapflow/compare/v2.0.2...v2.0.3) (2026-09-14)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * bound the chunked download loop by the reported message size ([fc9b4eb](https://github.com/postalsys/imapflow/commit/fc9b4ebdcc15fbc952fb2a18d98a723511e384ab))
9
+ * stop chunked download when the server ignores the partial spec ([#396](https://github.com/postalsys/imapflow/issues/396)) ([e216fde](https://github.com/postalsys/imapflow/commit/e216fdeaad4724bdeead6f346e207cd3d75a20fd))
10
+
11
+ ## [2.0.2](https://github.com/postalsys/imapflow/compare/v2.0.1...v2.0.2) (2026-09-10)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * skip LSUB once a server has rejected ENABLE over the IMAP4rev2 it advertises ([1c4bca2](https://github.com/postalsys/imapflow/commit/1c4bca2349ed05d5cb61a85abb4152630ceb3d73))
17
+
3
18
  ## [2.0.1](https://github.com/postalsys/imapflow/compare/v2.0.0...v2.0.1) (2026-09-10)
4
19
 
5
20
 
@@ -1199,6 +1199,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1199
1199
  // rejected, so it stays an IMAP4rev1 session (RFC 9051 Appendix A)
1200
1200
  // with an advertisement the server does not implement - see skipRev2
1201
1201
  this.skipRev2 = true;
1202
+ this.skipLsub = true;
1202
1203
  }
1203
1204
  // RFC 5161 requires servers to ignore unknown ENABLE arguments, but a
1204
1205
  // broken implementation may reject the whole command over IMAP4rev2 -
@@ -3028,7 +3029,22 @@ class ImapFlow extends node_events_1.EventEmitter {
3028
3029
  return {};
3029
3030
  }
3030
3031
  processed += chunk.length;
3031
- hasMore = chunk.length >= chunkSize;
3032
+ // A compliant server returns at most `chunkSize` bytes for a partial
3033
+ // request. Some servers (Tencent Exmail among them) ignore the partial
3034
+ // spec and answer every request with the complete part. That chunk is
3035
+ // then larger than requested, so treating it as "full, keep going"
3036
+ // would advance the offset past the end forever and never see a short
3037
+ // chunk. An oversized answer already contains the whole part - stop.
3038
+ hasMore = chunk.length === chunkSize;
3039
+ if (chunk.length > chunkSize) {
3040
+ this.log.warn({
3041
+ msg: 'Server returned more than the requested window, treating the part as complete',
3042
+ chunkSize,
3043
+ received: chunk.length,
3044
+ processed,
3045
+ cid: this.id
3046
+ });
3047
+ }
3032
3048
  let result = { chunk };
3033
3049
  if (query.size) {
3034
3050
  result.response = response;
@@ -3197,11 +3213,30 @@ class ImapFlow extends node_events_1.EventEmitter {
3197
3213
  }
3198
3214
  return stream.write(chunk);
3199
3215
  };
3216
+ // Ceiling on how many bytes one download may pull off the wire, as the backstop for the
3217
+ // partial-ignoring servers above: a part whose size happens to equal chunkSize exactly
3218
+ // comes back looking like a full window every time, so no test over chunk lengths can end
3219
+ // that loop. RFC822.SIZE bounds any part of the message; doubled for servers that count
3220
+ // line endings differently than they deliver, plus one window so a download sitting right
3221
+ // at the bound still gets its terminating chunk. Infinity when the server reported no
3222
+ // size, which leaves the loop bounded by maxBytes alone.
3223
+ let maxTotalBytes = (0, limited_passthrough_js_1.normalizeByteLimit)(meta.expectedSize ? meta.expectedSize * 2 + chunkSize : 0);
3200
3224
  // Fetch remaining chunks in a loop, writing each to the decoder stream.
3201
- // Stops when the server returns a short chunk (< chunkSize), the byte
3202
- // limiter is satisfied, or the consumer destroys the output stream.
3225
+ // Stops when the server returns a short chunk (< chunkSize), answers with more than the
3226
+ // requested window, the byte limiter is satisfied, or the consumer destroys the output
3227
+ // stream. Throws when the ceiling above is crossed.
3203
3228
  let fetchAllParts = async () => {
3204
3229
  while (hasMore && !isLimited() && !fetchAborted) {
3230
+ if (processed >= maxTotalBytes) {
3231
+ // Loud on purpose. Everything written downstream by this point holds
3232
+ // duplicated content, and a quiet stop is indistinguishable from a clean EOF,
3233
+ // so the consumer would store a corrupt body believing it intact.
3234
+ let err = new Error('Download exceeded the expected message size');
3235
+ err.code = 'DownloadOverflow';
3236
+ err.maxSize = maxTotalBytes;
3237
+ err.cid = this.id;
3238
+ throw err;
3239
+ }
3205
3240
  let { chunk } = await getNextPart();
3206
3241
  if (!chunk || fetchAborted) {
3207
3242
  break;
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.0.1";
2
+ export declare const version = "2.0.3";
3
3
  export declare const homepage = "https://imapflow.com/";
@@ -3,5 +3,5 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.homepage = exports.version = exports.name = void 0;
5
5
  exports.name = 'imapflow';
6
- exports.version = '2.0.1';
6
+ exports.version = '2.0.3';
7
7
  exports.homepage = 'https://imapflow.com/';
@@ -1159,6 +1159,7 @@ export class ImapFlow extends EventEmitter {
1159
1159
  // rejected, so it stays an IMAP4rev1 session (RFC 9051 Appendix A)
1160
1160
  // with an advertisement the server does not implement - see skipRev2
1161
1161
  this.skipRev2 = true;
1162
+ this.skipLsub = true;
1162
1163
  }
1163
1164
  // RFC 5161 requires servers to ignore unknown ENABLE arguments, but a
1164
1165
  // broken implementation may reject the whole command over IMAP4rev2 -
@@ -2988,7 +2989,22 @@ export class ImapFlow extends EventEmitter {
2988
2989
  return {};
2989
2990
  }
2990
2991
  processed += chunk.length;
2991
- hasMore = chunk.length >= chunkSize;
2992
+ // A compliant server returns at most `chunkSize` bytes for a partial
2993
+ // request. Some servers (Tencent Exmail among them) ignore the partial
2994
+ // spec and answer every request with the complete part. That chunk is
2995
+ // then larger than requested, so treating it as "full, keep going"
2996
+ // would advance the offset past the end forever and never see a short
2997
+ // chunk. An oversized answer already contains the whole part - stop.
2998
+ hasMore = chunk.length === chunkSize;
2999
+ if (chunk.length > chunkSize) {
3000
+ this.log.warn({
3001
+ msg: 'Server returned more than the requested window, treating the part as complete',
3002
+ chunkSize,
3003
+ received: chunk.length,
3004
+ processed,
3005
+ cid: this.id
3006
+ });
3007
+ }
2992
3008
  let result = { chunk };
2993
3009
  if (query.size) {
2994
3010
  result.response = response;
@@ -3157,11 +3173,30 @@ export class ImapFlow extends EventEmitter {
3157
3173
  }
3158
3174
  return stream.write(chunk);
3159
3175
  };
3176
+ // Ceiling on how many bytes one download may pull off the wire, as the backstop for the
3177
+ // partial-ignoring servers above: a part whose size happens to equal chunkSize exactly
3178
+ // comes back looking like a full window every time, so no test over chunk lengths can end
3179
+ // that loop. RFC822.SIZE bounds any part of the message; doubled for servers that count
3180
+ // line endings differently than they deliver, plus one window so a download sitting right
3181
+ // at the bound still gets its terminating chunk. Infinity when the server reported no
3182
+ // size, which leaves the loop bounded by maxBytes alone.
3183
+ let maxTotalBytes = normalizeByteLimit(meta.expectedSize ? meta.expectedSize * 2 + chunkSize : 0);
3160
3184
  // Fetch remaining chunks in a loop, writing each to the decoder stream.
3161
- // Stops when the server returns a short chunk (< chunkSize), the byte
3162
- // limiter is satisfied, or the consumer destroys the output stream.
3185
+ // Stops when the server returns a short chunk (< chunkSize), answers with more than the
3186
+ // requested window, the byte limiter is satisfied, or the consumer destroys the output
3187
+ // stream. Throws when the ceiling above is crossed.
3163
3188
  let fetchAllParts = async () => {
3164
3189
  while (hasMore && !isLimited() && !fetchAborted) {
3190
+ if (processed >= maxTotalBytes) {
3191
+ // Loud on purpose. Everything written downstream by this point holds
3192
+ // duplicated content, and a quiet stop is indistinguishable from a clean EOF,
3193
+ // so the consumer would store a corrupt body believing it intact.
3194
+ let err = new Error('Download exceeded the expected message size');
3195
+ err.code = 'DownloadOverflow';
3196
+ err.maxSize = maxTotalBytes;
3197
+ err.cid = this.id;
3198
+ throw err;
3199
+ }
3165
3200
  let { chunk } = await getNextPart();
3166
3201
  if (!chunk || fetchAborted) {
3167
3202
  break;
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.0.1";
2
+ export declare const version = "2.0.3";
3
3
  export declare const homepage = "https://imapflow.com/";
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/build.js from package.json. Do not edit by hand.
2
2
  export const name = 'imapflow';
3
- export const version = '2.0.1';
3
+ export const version = '2.0.3';
4
4
  export const homepage = 'https://imapflow.com/';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "IMAP Client for Node",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/imap-flow.js",