imapflow 2.0.2 → 2.0.4

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.4](https://github.com/postalsys/imapflow/compare/v2.0.3...v2.0.4) (2026-09-15)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **deps:** update libmime to 5.4.4 and encoding-japanese to 2.4.0 ([ffa0f93](https://github.com/postalsys/imapflow/commit/ffa0f93a60e7b17f8bd6f6920c158ee308b95cee))
9
+
10
+ ## [2.0.3](https://github.com/postalsys/imapflow/compare/v2.0.2...v2.0.3) (2026-09-14)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * bound the chunked download loop by the reported message size ([fc9b4eb](https://github.com/postalsys/imapflow/commit/fc9b4ebdcc15fbc952fb2a18d98a723511e384ab))
16
+ * 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))
17
+
3
18
  ## [2.0.2](https://github.com/postalsys/imapflow/compare/v2.0.1...v2.0.2) (2026-09-10)
4
19
 
5
20
 
@@ -3029,7 +3029,22 @@ class ImapFlow extends node_events_1.EventEmitter {
3029
3029
  return {};
3030
3030
  }
3031
3031
  processed += chunk.length;
3032
- 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
+ }
3033
3048
  let result = { chunk };
3034
3049
  if (query.size) {
3035
3050
  result.response = response;
@@ -3198,11 +3213,30 @@ class ImapFlow extends node_events_1.EventEmitter {
3198
3213
  }
3199
3214
  return stream.write(chunk);
3200
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);
3201
3224
  // Fetch remaining chunks in a loop, writing each to the decoder stream.
3202
- // Stops when the server returns a short chunk (< chunkSize), the byte
3203
- // 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.
3204
3228
  let fetchAllParts = async () => {
3205
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
+ }
3206
3240
  let { chunk } = await getNextPart();
3207
3241
  if (!chunk || fetchAborted) {
3208
3242
  break;
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.0.2";
2
+ export declare const version = "2.0.4";
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.2';
6
+ exports.version = '2.0.4';
7
7
  exports.homepage = 'https://imapflow.com/';
@@ -2989,7 +2989,22 @@ export class ImapFlow extends EventEmitter {
2989
2989
  return {};
2990
2990
  }
2991
2991
  processed += chunk.length;
2992
- 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
+ }
2993
3008
  let result = { chunk };
2994
3009
  if (query.size) {
2995
3010
  result.response = response;
@@ -3158,11 +3173,30 @@ export class ImapFlow extends EventEmitter {
3158
3173
  }
3159
3174
  return stream.write(chunk);
3160
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);
3161
3184
  // Fetch remaining chunks in a loop, writing each to the decoder stream.
3162
- // Stops when the server returns a short chunk (< chunkSize), the byte
3163
- // 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.
3164
3188
  let fetchAllParts = async () => {
3165
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
+ }
3166
3200
  let { chunk } = await getNextPart();
3167
3201
  if (!chunk || fetchAborted) {
3168
3202
  break;
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.0.2";
2
+ export declare const version = "2.0.4";
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.2';
3
+ export const version = '2.0.4';
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.2",
3
+ "version": "2.0.4",
4
4
  "description": "IMAP Client for Node",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/imap-flow.js",
@@ -77,15 +77,15 @@
77
77
  "tsx": "4.23.13",
78
78
  "types-node-legacy": "npm:@types/node@20.0.0",
79
79
  "typescript": "6.0.3",
80
- "typescript-eslint": "8.69.0",
81
- "wrangler": "4.129.0"
80
+ "typescript-eslint": "8.70.0",
81
+ "wrangler": "4.131.2"
82
82
  },
83
83
  "dependencies": {
84
84
  "@zone-eu/mailsplit": "5.4.16",
85
- "encoding-japanese": "2.3.0",
85
+ "encoding-japanese": "2.4.0",
86
86
  "iconv-lite": "0.7.3",
87
87
  "libbase64": "1.3.0",
88
- "libmime": "5.4.3",
88
+ "libmime": "5.4.4",
89
89
  "libqp": "2.1.1",
90
90
  "pino": "10.3.1",
91
91
  "socks": "2.8.10"