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 +15 -0
- package/dist/cjs/imap-flow.js +38 -3
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/esm/imap-flow.js +38 -3
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/package.json +1 -1
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
|
|
package/dist/cjs/imap-flow.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
3202
|
-
// limiter is satisfied, or the consumer destroys the output
|
|
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;
|
package/dist/cjs/package-info.js
CHANGED
package/dist/esm/imap-flow.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
3162
|
-
// limiter is satisfied, or the consumer destroys the output
|
|
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;
|
package/dist/esm/package-info.js
CHANGED