imapflow 2.0.7 → 2.1.0
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 +29 -0
- package/dist/cjs/commands/append.js +12 -12
- package/dist/cjs/commands/authenticate.d.ts +3 -8
- package/dist/cjs/commands/close.js +2 -1
- package/dist/cjs/commands/copy.js +4 -4
- package/dist/cjs/commands/create.js +2 -3
- package/dist/cjs/commands/delete.js +4 -4
- package/dist/cjs/commands/expunge.js +8 -5
- package/dist/cjs/commands/fetch.js +12 -10
- package/dist/cjs/commands/idle.js +6 -2
- package/dist/cjs/commands/list.js +22 -18
- package/dist/cjs/commands/move.js +11 -6
- package/dist/cjs/commands/namespace.js +1 -1
- package/dist/cjs/commands/quota.js +10 -9
- package/dist/cjs/commands/rename.js +4 -4
- package/dist/cjs/commands/search.js +7 -8
- package/dist/cjs/commands/select.js +12 -9
- package/dist/cjs/commands/status.js +11 -11
- package/dist/cjs/commands/store.js +5 -5
- package/dist/cjs/commands/subscribe.js +2 -17
- package/dist/cjs/commands/subscription.d.ts +10 -0
- package/dist/cjs/commands/subscription.js +29 -0
- package/dist/cjs/commands/unsubscribe.js +2 -17
- package/dist/cjs/download.d.ts +22 -0
- package/dist/cjs/download.js +588 -0
- package/dist/cjs/errors.d.ts +50 -1
- package/dist/cjs/errors.js +53 -1
- package/dist/cjs/handler/imap-compiler.js +1 -1
- package/dist/cjs/handler/imap-stream.d.ts +13 -2
- package/dist/cjs/handler/imap-stream.js +51 -30
- package/dist/cjs/handler/parser-instance.js +2 -2
- package/dist/cjs/handler/token-parser.js +15 -9
- package/dist/cjs/imap-flow.d.ts +30 -84
- package/dist/cjs/imap-flow.js +282 -734
- package/dist/cjs/jp-decoder.js +1 -1
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +3 -3
- package/dist/cjs/search-compiler.js +5 -12
- package/dist/cjs/tools.d.ts +52 -11
- package/dist/cjs/tools.js +86 -23
- package/dist/cjs/types.d.ts +30 -16
- package/dist/esm/commands/append.js +13 -13
- package/dist/esm/commands/authenticate.d.ts +3 -8
- package/dist/esm/commands/close.js +2 -1
- package/dist/esm/commands/copy.js +5 -5
- package/dist/esm/commands/create.js +3 -4
- package/dist/esm/commands/delete.js +5 -5
- package/dist/esm/commands/expunge.js +9 -6
- package/dist/esm/commands/fetch.js +13 -11
- package/dist/esm/commands/idle.js +7 -3
- package/dist/esm/commands/list.js +22 -18
- package/dist/esm/commands/move.js +12 -7
- package/dist/esm/commands/namespace.js +2 -2
- package/dist/esm/commands/quota.js +11 -10
- package/dist/esm/commands/rename.js +5 -5
- package/dist/esm/commands/search.js +8 -9
- package/dist/esm/commands/select.js +13 -10
- package/dist/esm/commands/status.js +12 -12
- package/dist/esm/commands/store.js +6 -6
- package/dist/esm/commands/subscribe.js +2 -17
- package/dist/esm/commands/subscription.d.ts +10 -0
- package/dist/esm/commands/subscription.js +26 -0
- package/dist/esm/commands/unsubscribe.js +2 -17
- package/dist/esm/download.d.ts +22 -0
- package/dist/esm/download.js +581 -0
- package/dist/esm/errors.d.ts +50 -1
- package/dist/esm/errors.js +52 -0
- package/dist/esm/handler/imap-compiler.js +1 -1
- package/dist/esm/handler/imap-stream.d.ts +13 -2
- package/dist/esm/handler/imap-stream.js +51 -30
- package/dist/esm/handler/parser-instance.js +2 -2
- package/dist/esm/handler/token-parser.js +15 -9
- package/dist/esm/imap-flow.d.ts +30 -84
- package/dist/esm/imap-flow.js +282 -735
- package/dist/esm/jp-decoder.js +1 -1
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +3 -3
- package/dist/esm/search-compiler.js +5 -12
- package/dist/esm/tools.d.ts +52 -11
- package/dist/esm/tools.js +79 -21
- package/dist/esm/types.d.ts +30 -16
- package/package.json +4 -4
package/dist/cjs/imap-flow.js
CHANGED
|
@@ -39,30 +39,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
39
39
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
40
|
};
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
-
exports.ImapFlow = exports.AuthenticationFailure = void 0;
|
|
42
|
+
exports.ImapFlow = exports.ImapFlowErrorCode = exports.AuthenticationFailure = void 0;
|
|
43
43
|
const node_tls_1 = __importDefault(require("node:tls"));
|
|
44
44
|
const node_net_1 = __importDefault(require("node:net"));
|
|
45
45
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
46
46
|
const node_zlib_1 = __importDefault(require("node:zlib"));
|
|
47
47
|
const node_events_1 = require("node:events");
|
|
48
48
|
const node_stream_1 = require("node:stream");
|
|
49
|
-
const libmime_1 = __importDefault(require("libmime"));
|
|
50
|
-
const libqp_1 = __importDefault(require("libqp"));
|
|
51
|
-
const libbase64_1 = __importDefault(require("libbase64"));
|
|
52
|
-
const mailsplit_1 = require("@zone-eu/mailsplit");
|
|
53
|
-
const flowed_decoder_js_1 = __importDefault(require("@zone-eu/mailsplit/lib/flowed-decoder.js"));
|
|
54
49
|
const logger_js_1 = __importDefault(require("./logger.js"));
|
|
55
50
|
const packageInfo = __importStar(require("./package-info.js"));
|
|
56
|
-
const limited_passthrough_js_1 = require("./limited-passthrough.js");
|
|
57
51
|
const imap_stream_js_1 = require("./handler/imap-stream.js");
|
|
58
52
|
const imap_handler_js_1 = require("./handler/imap-handler.js");
|
|
59
53
|
const proxy_connection_js_1 = require("./proxy-connection.js");
|
|
60
54
|
const connection_deadline_js_1 = require("./connection-deadline.js");
|
|
55
|
+
const download_js_1 = require("./download.js");
|
|
61
56
|
const errors_js_1 = require("./errors.js");
|
|
62
57
|
const imap_commands_js_1 = __importDefault(require("./imap-commands.js"));
|
|
63
58
|
const tools_js_1 = require("./tools.js");
|
|
64
59
|
var errors_js_2 = require("./errors.js");
|
|
65
60
|
Object.defineProperty(exports, "AuthenticationFailure", { enumerable: true, get: function () { return errors_js_2.AuthenticationFailure; } });
|
|
61
|
+
Object.defineProperty(exports, "ImapFlowErrorCode", { enumerable: true, get: function () { return errors_js_2.ImapFlowErrorCode; } });
|
|
66
62
|
const GREETING_TIMEOUT = 16 * 1000;
|
|
67
63
|
const UPGRADE_TIMEOUT = 10 * 1000;
|
|
68
64
|
const SOCKET_TIMEOUT = 5 * 60 * 1000;
|
|
@@ -200,6 +196,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
200
196
|
* Current module version as a static class property
|
|
201
197
|
*/
|
|
202
198
|
static { this.version = packageInfo.version; }
|
|
199
|
+
/**
|
|
200
|
+
* Creates a client for one IMAP connection. Nothing is sent before `connect()` is called
|
|
201
|
+
*/
|
|
203
202
|
constructor(options) {
|
|
204
203
|
super({ captureRejections: true });
|
|
205
204
|
this.options = options || {};
|
|
@@ -350,7 +349,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
350
349
|
rid = '0'.repeat(20 - rid.length) + rid;
|
|
351
350
|
}
|
|
352
351
|
if (rid.length > 20) {
|
|
353
|
-
rid = rid.
|
|
352
|
+
rid = rid.substring(0, 20);
|
|
354
353
|
}
|
|
355
354
|
return rid;
|
|
356
355
|
}
|
|
@@ -938,11 +937,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
938
937
|
break;
|
|
939
938
|
case 'NO':
|
|
940
939
|
case 'BAD': {
|
|
941
|
-
let txt = parsed.attributes
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
.map(val => val.value.trim())
|
|
945
|
-
.join(' ');
|
|
940
|
+
let txt = (0, tools_js_1.getTextValues)(parsed.attributes)
|
|
941
|
+
.map(val => val.trim())
|
|
942
|
+
.join(' ');
|
|
946
943
|
let err = new Error('Command failed');
|
|
947
944
|
err.response = parsed;
|
|
948
945
|
err.responseStatus = parsed.command.toUpperCase();
|
|
@@ -973,7 +970,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
973
970
|
// Example: "tag BAD Request is throttled. Suggested Backoff Time: 92415 milliseconds"
|
|
974
971
|
if (/Request is throttled/i.test(txt) && /Backoff Time/i.test(txt)) {
|
|
975
972
|
let throttlingMatch = txt.match(/Backoff Time[:=\s]+(\d+)/i);
|
|
976
|
-
if (throttlingMatch
|
|
973
|
+
if (throttlingMatch) {
|
|
977
974
|
throttleDelay = Number(throttlingMatch[1]);
|
|
978
975
|
}
|
|
979
976
|
}
|
|
@@ -981,8 +978,14 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
981
978
|
if (throttleDelay) {
|
|
982
979
|
err.code = 'ETHROTTLE';
|
|
983
980
|
err.throttleReset = throttleDelay;
|
|
984
|
-
// The server-suggested delay can be very large, so throttleWait() caps it
|
|
985
|
-
|
|
981
|
+
// The server-suggested delay can be very large, so throttleWait() caps it.
|
|
982
|
+
// The reader loop is parked for the whole wait, so it also stays well
|
|
983
|
+
// inside the socket inactivity timeout: a wait that outlasted it would
|
|
984
|
+
// fire the timeout handler, and the keepalive NOOP it sends can not be
|
|
985
|
+
// read while the loop is parked, so the connection would be torn down.
|
|
986
|
+
// A caller that retries (fetch) waits out the rest of the hint itself.
|
|
987
|
+
let delayResponse = Math.min(throttleDelay, MAX_THROTTLE_DELAY, Math.floor(this.socketTimeout / 2));
|
|
988
|
+
err.throttleWaited = delayResponse;
|
|
986
989
|
this.log.warn({ msg: 'Throttling detected', cid: this.id, throttleDelay, delayResponse, err });
|
|
987
990
|
let aborted = await this.throttleWait(delayResponse);
|
|
988
991
|
if (aborted) {
|
|
@@ -1281,7 +1284,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1281
1284
|
let chunk;
|
|
1282
1285
|
while (this.writeSocket && (chunk = this.writeSocket.read()) !== null) {
|
|
1283
1286
|
if (this._deflate && this._deflate.write(chunk) === false) {
|
|
1284
|
-
this._deflate.once('drain',
|
|
1287
|
+
this._deflate.once('drain', () => {
|
|
1288
|
+
void readNext();
|
|
1289
|
+
});
|
|
1285
1290
|
return;
|
|
1286
1291
|
}
|
|
1287
1292
|
// Yield to event loop every 100 chunks to prevent CPU blocking
|
|
@@ -1307,7 +1312,8 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1307
1312
|
};
|
|
1308
1313
|
writeSocket.on('readable', () => {
|
|
1309
1314
|
if (!reading && this.writeSocket) {
|
|
1310
|
-
readNext()
|
|
1315
|
+
// readNext() reports its own failures, the returned promise never rejects
|
|
1316
|
+
void readNext();
|
|
1311
1317
|
}
|
|
1312
1318
|
});
|
|
1313
1319
|
writeSocket.on('error', err => {
|
|
@@ -1596,6 +1602,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1596
1602
|
/** @internal */
|
|
1597
1603
|
beginSession(onUnhandledError) {
|
|
1598
1604
|
(0, tools_js_1.clearTimer)(this.greetingTimeout);
|
|
1605
|
+
this.greetingReceived = true;
|
|
1599
1606
|
this.untaggedHandlers.OK = null;
|
|
1600
1607
|
this.untaggedHandlers.PREAUTH = null;
|
|
1601
1608
|
if (this.isClosed) {
|
|
@@ -1625,9 +1632,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1625
1632
|
}
|
|
1626
1633
|
/** @internal */
|
|
1627
1634
|
async initialOK(message) {
|
|
1628
|
-
this.greeting = (message.attributes
|
|
1629
|
-
.filter(entry => entry.type === 'TEXT')
|
|
1630
|
-
.map(entry => entry.value)
|
|
1635
|
+
this.greeting = (0, tools_js_1.getTextValues)(message.attributes)
|
|
1631
1636
|
.filter(entry => entry)
|
|
1632
1637
|
.join('');
|
|
1633
1638
|
// ALWAYS emit the error so users can handle it
|
|
@@ -1651,14 +1656,18 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1651
1656
|
async serverBye(parsed) {
|
|
1652
1657
|
// Extract BYE reason from response for better error messages
|
|
1653
1658
|
let reason = parsed &&
|
|
1654
|
-
parsed.attributes
|
|
1655
|
-
|
|
1656
|
-
.filter(val => val.type === 'TEXT')
|
|
1657
|
-
.map(val => val.value.trim())
|
|
1659
|
+
(0, tools_js_1.getTextValues)(parsed.attributes)
|
|
1660
|
+
.map(val => val.trim())
|
|
1658
1661
|
.join(' ');
|
|
1659
1662
|
this.byeReason = reason || 'Server closed connection';
|
|
1660
1663
|
this.untaggedHandlers.BYE = null;
|
|
1661
1664
|
this.state = this.states.LOGOUT;
|
|
1665
|
+
// A BYE greeting rejects the connection outright. Do not wait for the server to close
|
|
1666
|
+
// the socket: one that keeps it open would leave connect() pending until the greeting
|
|
1667
|
+
// timeout.
|
|
1668
|
+
if (!this.greetingReceived) {
|
|
1669
|
+
this.closeAfter();
|
|
1670
|
+
}
|
|
1662
1671
|
}
|
|
1663
1672
|
// Drops every capability-derived field together - the counterpart of
|
|
1664
1673
|
// updateCapabilitiesFromRaw() below, which sets them together. rawCapabilities is
|
|
@@ -1841,7 +1850,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1841
1850
|
// Replace "*" with the actual message count. Some servers reject bare "*"
|
|
1842
1851
|
// in certain commands, and this also forces a sequence query (not UID).
|
|
1843
1852
|
if (value === '*') {
|
|
1844
|
-
if (!this.mailbox.exists) {
|
|
1853
|
+
if (!this.mailbox || !this.mailbox.exists) {
|
|
1845
1854
|
return false;
|
|
1846
1855
|
}
|
|
1847
1856
|
value = this.mailbox.exists.toString();
|
|
@@ -1891,7 +1900,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1891
1900
|
/** @internal */
|
|
1892
1901
|
autoidle() {
|
|
1893
1902
|
(0, tools_js_1.clearTimer)(this.idleStartTimer);
|
|
1894
|
-
if (this.options.disableAutoIdle || this.state !== this.states.SELECTED) {
|
|
1903
|
+
if (this.options.disableAutoIdle || !this.usable || this.state !== this.states.SELECTED) {
|
|
1895
1904
|
return;
|
|
1896
1905
|
}
|
|
1897
1906
|
if (this.connectionBusy()) {
|
|
@@ -1903,7 +1912,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1903
1912
|
// missed clearTimeout would inject IDLE between a caller's own commands. Declining
|
|
1904
1913
|
// postpones rather than cancels: whatever made the connection busy calls autoidle()
|
|
1905
1914
|
// again when it finishes.
|
|
1906
|
-
if (this.state !== this.states.SELECTED || this.connectionBusy()) {
|
|
1915
|
+
if (!this.usable || this.state !== this.states.SELECTED || this.connectionBusy()) {
|
|
1907
1916
|
return;
|
|
1908
1917
|
}
|
|
1909
1918
|
this.idle().catch(err => (0, tools_js_1.logConnectionError)(this, 'Auto-IDLE failed', err));
|
|
@@ -2109,50 +2118,14 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2109
2118
|
*/
|
|
2110
2119
|
close() {
|
|
2111
2120
|
try {
|
|
2112
|
-
|
|
2113
|
-
(0, tools_js_1.clearTimer)(this.idleStartTimer);
|
|
2114
|
-
(0, tools_js_1.clearTimer)(this.upgradeTimeout);
|
|
2115
|
-
(0, tools_js_1.clearTimer)(this.connectTimeout);
|
|
2116
|
-
(0, tools_js_1.clearTimer)(this.greetingTimeout);
|
|
2117
|
-
// Abort every in-flight throttle back-off so each waiter unblocks and its request is
|
|
2118
|
-
// settled promptly rather than after the full delay.
|
|
2119
|
-
for (let entry of this._throttleWaits) {
|
|
2120
|
-
(0, tools_js_1.clearTimer)(entry.timer);
|
|
2121
|
-
entry.resolve(true);
|
|
2122
|
-
}
|
|
2123
|
-
this._throttleWaits.clear();
|
|
2121
|
+
this.closeTimers();
|
|
2124
2122
|
this.usable = false;
|
|
2125
2123
|
// close() takes over ownership of the idling state: dropping the session token means a
|
|
2126
2124
|
// poll or IDLE that unwinds after this point sees that it no longer owns the flag and
|
|
2127
2125
|
// leaves it alone (see claimIdling() in commands/idle.ts).
|
|
2128
2126
|
this._idleSession = null;
|
|
2129
2127
|
this.idling = false;
|
|
2130
|
-
|
|
2131
|
-
// path, otherwise the upgrade promise (and the session it belongs to) stays pending
|
|
2132
|
-
// for the lifetime of the process.
|
|
2133
|
-
if (typeof this._upgradeReject === 'function') {
|
|
2134
|
-
let reject = this._upgradeReject;
|
|
2135
|
-
this._upgradeReject = null;
|
|
2136
|
-
reject(this.createNoConnectionError(false, { rejectedFrom: 'upgrade' }));
|
|
2137
|
-
}
|
|
2138
|
-
if (typeof this.initialReject === 'function' && !this.options.verifyOnly) {
|
|
2139
|
-
(0, tools_js_1.clearTimer)(this.greetingTimeout);
|
|
2140
|
-
let reject = this.initialReject;
|
|
2141
|
-
this.initialResolve = false;
|
|
2142
|
-
this.initialReject = false;
|
|
2143
|
-
let err = new Error('Unexpected close');
|
|
2144
|
-
/* c8 ignore next */ // closing a pending connect over an already-secure socket (the TLS branch) is not separately exercised
|
|
2145
|
-
err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
|
|
2146
|
-
// Surface the server's BYE reason (e.g. "Too many connections") when the
|
|
2147
|
-
// connection was closed by an untagged BYE, so the caller sees why.
|
|
2148
|
-
if (this.byeReason) {
|
|
2149
|
-
err.reason = this.byeReason;
|
|
2150
|
-
}
|
|
2151
|
-
// Synchronous rejection is safe: connectPromise was built by guardedPromise(),
|
|
2152
|
-
// so the rejection is already observed. close() is synchronous, so all cleanup
|
|
2153
|
-
// completes before any microtask rejection handler runs.
|
|
2154
|
-
reject(err);
|
|
2155
|
-
}
|
|
2128
|
+
this.closeConnectSteps();
|
|
2156
2129
|
if (typeof this.preCheck === 'function') {
|
|
2157
2130
|
// Runs while the connection is being torn down, so the rejection this sees is
|
|
2158
2131
|
// almost always the NoConnection close() is about to raise itself.
|
|
@@ -2179,99 +2152,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2179
2152
|
}
|
|
2180
2153
|
this.preCheck = false;
|
|
2181
2154
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
if (this.currentRequest && this.requestTagMap.has(this.currentRequest.tag)) {
|
|
2186
|
-
let tag = this.currentRequest.tag;
|
|
2187
|
-
let request = this.requestTagMap.get(tag);
|
|
2188
|
-
if (request) {
|
|
2189
|
-
this.requestTagMap.delete(tag);
|
|
2190
|
-
pendingRequests.push(request);
|
|
2191
|
-
}
|
|
2192
|
-
this.currentRequest = false;
|
|
2193
|
-
}
|
|
2194
|
-
// reject all other pending commands
|
|
2195
|
-
while (this.requestQueue.length) {
|
|
2196
|
-
let req = this.requestQueue.shift();
|
|
2197
|
-
if (req && this.requestTagMap.has(req.tag)) {
|
|
2198
|
-
let request = this.requestTagMap.get(req.tag);
|
|
2199
|
-
if (request) {
|
|
2200
|
-
this.requestTagMap.delete(req.tag);
|
|
2201
|
-
pendingRequests.push(request);
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
// Reject pending requests and locks synchronously. Every promise rejected here was
|
|
2206
|
-
// built by guardedPromise(), so its rejection is already observed and cannot trigger
|
|
2207
|
-
// unhandledRejection. close() is synchronous, so all remaining cleanup runs before
|
|
2208
|
-
// any microtask rejection handler fires.
|
|
2209
|
-
//
|
|
2210
|
-
// The error travels on, though, through await chains and .then() links that
|
|
2211
|
-
// guardedPromise() knows nothing about. Read a crash stack ending here as "this is
|
|
2212
|
-
// the value that escaped", never as "this is the promise that escaped".
|
|
2213
|
-
let byeReason = this.byeReason;
|
|
2214
|
-
for (let request of pendingRequests) {
|
|
2215
|
-
request.reject(this.createNoConnectionError(byeReason, { rejectedFrom: 'pendingRequest', command: request.command }));
|
|
2216
|
-
}
|
|
2217
|
-
// Clear current lock - holder will see errors when they try operations.
|
|
2218
|
-
// Also clear the held-lock diagnostic timer so it doesn't fire post-close.
|
|
2219
|
-
if (this.currentLock && this.currentLock.heldWarnTimer) {
|
|
2220
|
-
(0, tools_js_1.clearTimer)(this.currentLock.heldWarnTimer);
|
|
2221
|
-
this.currentLock.heldWarnTimer = null;
|
|
2222
|
-
}
|
|
2223
|
-
this.currentLock = false;
|
|
2224
|
-
if (this.locks && this.locks.length) {
|
|
2225
|
-
let pendingLocks = this.locks.splice(0); // Take all locks and clear the array
|
|
2226
|
-
for (let lock of pendingLocks) {
|
|
2227
|
-
if (lock.acquireTimer) {
|
|
2228
|
-
(0, tools_js_1.clearTimer)(lock.acquireTimer);
|
|
2229
|
-
lock.acquireTimer = null;
|
|
2230
|
-
}
|
|
2231
|
-
if (typeof lock.reject === 'function') {
|
|
2232
|
-
lock.reject(this.createNoConnectionError(byeReason, { rejectedFrom: 'mailboxLock', path: lock.path }));
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
// cleanup compression streams if they exist
|
|
2237
|
-
if (this._inflate) {
|
|
2238
|
-
try {
|
|
2239
|
-
this._inflate.unpipe();
|
|
2240
|
-
this._inflate.destroy();
|
|
2241
|
-
this._inflate = null;
|
|
2242
|
-
}
|
|
2243
|
-
catch (err) {
|
|
2244
|
-
this.log.error({ err, msg: 'Failed to destroy inflate stream', cid: this.id });
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
if (this._deflate) {
|
|
2248
|
-
try {
|
|
2249
|
-
this._deflate.unpipe();
|
|
2250
|
-
this._deflate.destroy();
|
|
2251
|
-
this._deflate = null;
|
|
2252
|
-
}
|
|
2253
|
-
catch (err) {
|
|
2254
|
-
this.log.error({ err, msg: 'Failed to destroy deflate stream', cid: this.id });
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
// cleanup streamer
|
|
2258
|
-
if (this.streamer) {
|
|
2259
|
-
try {
|
|
2260
|
-
// remove our listeners explicitly by reference
|
|
2261
|
-
if (this.socketReadable) {
|
|
2262
|
-
this.streamer.removeListener('readable', this.socketReadable);
|
|
2263
|
-
}
|
|
2264
|
-
if (this._streamerErrorHandler) {
|
|
2265
|
-
this.streamer.removeListener('error', this._streamerErrorHandler);
|
|
2266
|
-
}
|
|
2267
|
-
if (!this.streamer.destroyed) {
|
|
2268
|
-
this.streamer.destroy();
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
catch (err) {
|
|
2272
|
-
this.log.error({ err, msg: 'Failed to cleanup streamer', cid: this.id });
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2155
|
+
this.closeRequests();
|
|
2156
|
+
this.closeLocks();
|
|
2157
|
+
this.closeStreams();
|
|
2275
2158
|
// clear socket handlers
|
|
2276
2159
|
this.clearSocketHandlers();
|
|
2277
2160
|
// clear cached data
|
|
@@ -2284,39 +2167,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2284
2167
|
// Set before teardown so a socket event that re-enters close() during destruction
|
|
2285
2168
|
// cannot run this block a second time.
|
|
2286
2169
|
this.isClosed = true;
|
|
2287
|
-
|
|
2288
|
-
// lifecycle, so each is destroyed exactly once:
|
|
2289
|
-
// 1. the compression PassThrough (writeSocket), if compression replaced it
|
|
2290
|
-
// 2. the raw socket, which is also writeSocket when compression is not active
|
|
2291
|
-
// The compression streams themselves were destroyed above.
|
|
2292
|
-
if (this.writeSocket && this.writeSocket !== this.socket && !this.writeSocket.destroyed) {
|
|
2293
|
-
try {
|
|
2294
|
-
this.writeSocket.destroy();
|
|
2295
|
-
}
|
|
2296
|
-
catch (err) {
|
|
2297
|
-
this.log.error({ err, cid: this.id });
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
if (this.socket && !this.socket.destroyed) {
|
|
2301
|
-
try {
|
|
2302
|
-
this.socket.destroy();
|
|
2303
|
-
}
|
|
2304
|
-
catch (err) {
|
|
2305
|
-
this.log.error({ err, cid: this.id });
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
// Null out all socket and handler references so the GC can collect
|
|
2309
|
-
// them even if the ImapFlow instance itself is still referenced.
|
|
2310
|
-
this.socket = null;
|
|
2311
|
-
this.writeSocket = null;
|
|
2312
|
-
this._inflate = null;
|
|
2313
|
-
this._deflate = null;
|
|
2314
|
-
this._streamerErrorHandler = null;
|
|
2315
|
-
this._connectErrorHandler = null;
|
|
2316
|
-
this._socketError = null;
|
|
2317
|
-
this._socketClose = null;
|
|
2318
|
-
this._socketEnd = null;
|
|
2319
|
-
this._socketTimeout = null;
|
|
2170
|
+
this.closeSockets();
|
|
2320
2171
|
this.log.debug({
|
|
2321
2172
|
msg: 'Connection closed',
|
|
2322
2173
|
cid: this.id,
|
|
@@ -2326,7 +2177,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2326
2177
|
// whether the session ended with a clean logout or a lost transport. Emitted before
|
|
2327
2178
|
// 'close' and only from the first close(), so no consumer sees it twice.
|
|
2328
2179
|
if (closedMailbox) {
|
|
2329
|
-
|
|
2180
|
+
(0, tools_js_1.emitSafe)(this, 'mailboxClose', closedMailbox);
|
|
2330
2181
|
}
|
|
2331
2182
|
this.emit('close');
|
|
2332
2183
|
}
|
|
@@ -2335,6 +2186,212 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2335
2186
|
this.log.error({ err: ex, cid: this.id });
|
|
2336
2187
|
}
|
|
2337
2188
|
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Part of close(): clears the pending timers and aborts every throttle back-off.
|
|
2191
|
+
*
|
|
2192
|
+
* @internal
|
|
2193
|
+
*/
|
|
2194
|
+
closeTimers() {
|
|
2195
|
+
// clear pending timers
|
|
2196
|
+
(0, tools_js_1.clearTimer)(this.idleStartTimer);
|
|
2197
|
+
(0, tools_js_1.clearTimer)(this.upgradeTimeout);
|
|
2198
|
+
(0, tools_js_1.clearTimer)(this.connectTimeout);
|
|
2199
|
+
(0, tools_js_1.clearTimer)(this.greetingTimeout);
|
|
2200
|
+
// Abort every in-flight throttle back-off so each waiter unblocks and its request is
|
|
2201
|
+
// settled promptly rather than after the full delay.
|
|
2202
|
+
for (let entry of this._throttleWaits) {
|
|
2203
|
+
(0, tools_js_1.clearTimer)(entry.timer);
|
|
2204
|
+
entry.resolve(true);
|
|
2205
|
+
}
|
|
2206
|
+
this._throttleWaits.clear();
|
|
2207
|
+
}
|
|
2208
|
+
/**
|
|
2209
|
+
* Part of close(): settles an in-flight STARTTLS upgrade and a pending connect().
|
|
2210
|
+
*
|
|
2211
|
+
* @internal
|
|
2212
|
+
*/
|
|
2213
|
+
closeConnectSteps() {
|
|
2214
|
+
// An in-flight STARTTLS upgrade has to be settled through its own single settlement
|
|
2215
|
+
// path, otherwise the upgrade promise (and the session it belongs to) stays pending
|
|
2216
|
+
// for the lifetime of the process.
|
|
2217
|
+
if (typeof this._upgradeReject === 'function') {
|
|
2218
|
+
let reject = this._upgradeReject;
|
|
2219
|
+
this._upgradeReject = null;
|
|
2220
|
+
reject(this.createNoConnectionError(false, { rejectedFrom: 'upgrade' }));
|
|
2221
|
+
}
|
|
2222
|
+
if (typeof this.initialReject === 'function' && !this.options.verifyOnly) {
|
|
2223
|
+
(0, tools_js_1.clearTimer)(this.greetingTimeout);
|
|
2224
|
+
let reject = this.initialReject;
|
|
2225
|
+
this.initialResolve = false;
|
|
2226
|
+
this.initialReject = false;
|
|
2227
|
+
let err = new Error('Unexpected close');
|
|
2228
|
+
/* c8 ignore next */ // closing a pending connect over an already-secure socket (the TLS branch) is not separately exercised
|
|
2229
|
+
err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
|
|
2230
|
+
// Surface the server's BYE reason (e.g. "Too many connections") when the
|
|
2231
|
+
// connection was closed by an untagged BYE, so the caller sees why.
|
|
2232
|
+
if (this.byeReason) {
|
|
2233
|
+
err.reason = this.byeReason;
|
|
2234
|
+
}
|
|
2235
|
+
// Synchronous rejection is safe: connectPromise was built by guardedPromise(),
|
|
2236
|
+
// so the rejection is already observed. close() is synchronous, so all cleanup
|
|
2237
|
+
// completes before any microtask rejection handler runs.
|
|
2238
|
+
reject(err);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Part of close(): rejects the command in flight and every queued command.
|
|
2243
|
+
*
|
|
2244
|
+
* @internal
|
|
2245
|
+
*/
|
|
2246
|
+
closeRequests() {
|
|
2247
|
+
// Collect all pending requests to reject
|
|
2248
|
+
let pendingRequests = [];
|
|
2249
|
+
// reject command that is currently processed
|
|
2250
|
+
if (this.currentRequest && this.requestTagMap.has(this.currentRequest.tag)) {
|
|
2251
|
+
let tag = this.currentRequest.tag;
|
|
2252
|
+
let request = this.requestTagMap.get(tag);
|
|
2253
|
+
if (request) {
|
|
2254
|
+
this.requestTagMap.delete(tag);
|
|
2255
|
+
pendingRequests.push(request);
|
|
2256
|
+
}
|
|
2257
|
+
this.currentRequest = false;
|
|
2258
|
+
}
|
|
2259
|
+
// reject all other pending commands
|
|
2260
|
+
while (this.requestQueue.length) {
|
|
2261
|
+
let req = this.requestQueue.shift();
|
|
2262
|
+
if (req && this.requestTagMap.has(req.tag)) {
|
|
2263
|
+
let request = this.requestTagMap.get(req.tag);
|
|
2264
|
+
if (request) {
|
|
2265
|
+
this.requestTagMap.delete(req.tag);
|
|
2266
|
+
pendingRequests.push(request);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
// Reject pending requests and locks synchronously. Every promise rejected here was
|
|
2271
|
+
// built by guardedPromise(), so its rejection is already observed and cannot trigger
|
|
2272
|
+
// unhandledRejection. close() is synchronous, so all remaining cleanup runs before
|
|
2273
|
+
// any microtask rejection handler fires.
|
|
2274
|
+
//
|
|
2275
|
+
// The error travels on, though, through await chains and .then() links that
|
|
2276
|
+
// guardedPromise() knows nothing about. Read a crash stack ending here as "this is
|
|
2277
|
+
// the value that escaped", never as "this is the promise that escaped".
|
|
2278
|
+
for (let request of pendingRequests) {
|
|
2279
|
+
request.reject(this.createNoConnectionError(this.byeReason, { rejectedFrom: 'pendingRequest', command: request.command }));
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Part of close(): drops the held lock and rejects every pending lock request.
|
|
2284
|
+
*
|
|
2285
|
+
* @internal
|
|
2286
|
+
*/
|
|
2287
|
+
closeLocks() {
|
|
2288
|
+
// Clear current lock - holder will see errors when they try operations.
|
|
2289
|
+
// Also clear the held-lock diagnostic timer so it doesn't fire post-close.
|
|
2290
|
+
if (this.currentLock && this.currentLock.heldWarnTimer) {
|
|
2291
|
+
(0, tools_js_1.clearTimer)(this.currentLock.heldWarnTimer);
|
|
2292
|
+
this.currentLock.heldWarnTimer = null;
|
|
2293
|
+
}
|
|
2294
|
+
this.currentLock = false;
|
|
2295
|
+
if (this.locks && this.locks.length) {
|
|
2296
|
+
let pendingLocks = this.locks.splice(0); // Take all locks and clear the array
|
|
2297
|
+
for (let lock of pendingLocks) {
|
|
2298
|
+
if (lock.acquireTimer) {
|
|
2299
|
+
(0, tools_js_1.clearTimer)(lock.acquireTimer);
|
|
2300
|
+
lock.acquireTimer = null;
|
|
2301
|
+
}
|
|
2302
|
+
if (typeof lock.reject === 'function') {
|
|
2303
|
+
lock.reject(this.createNoConnectionError(this.byeReason, { rejectedFrom: 'mailboxLock', path: lock.path }));
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* Part of close(): destroys the compression streams and the response streamer.
|
|
2310
|
+
*
|
|
2311
|
+
* @internal
|
|
2312
|
+
*/
|
|
2313
|
+
closeStreams() {
|
|
2314
|
+
// cleanup compression streams if they exist
|
|
2315
|
+
if (this._inflate) {
|
|
2316
|
+
try {
|
|
2317
|
+
this._inflate.unpipe();
|
|
2318
|
+
this._inflate.destroy();
|
|
2319
|
+
this._inflate = null;
|
|
2320
|
+
}
|
|
2321
|
+
catch (err) {
|
|
2322
|
+
this.log.error({ err, msg: 'Failed to destroy inflate stream', cid: this.id });
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
if (this._deflate) {
|
|
2326
|
+
try {
|
|
2327
|
+
this._deflate.unpipe();
|
|
2328
|
+
this._deflate.destroy();
|
|
2329
|
+
this._deflate = null;
|
|
2330
|
+
}
|
|
2331
|
+
catch (err) {
|
|
2332
|
+
this.log.error({ err, msg: 'Failed to destroy deflate stream', cid: this.id });
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
// cleanup streamer
|
|
2336
|
+
if (this.streamer) {
|
|
2337
|
+
try {
|
|
2338
|
+
// remove our listeners explicitly by reference
|
|
2339
|
+
if (this.socketReadable) {
|
|
2340
|
+
this.streamer.removeListener('readable', this.socketReadable);
|
|
2341
|
+
}
|
|
2342
|
+
if (this._streamerErrorHandler) {
|
|
2343
|
+
this.streamer.removeListener('error', this._streamerErrorHandler);
|
|
2344
|
+
}
|
|
2345
|
+
if (!this.streamer.destroyed) {
|
|
2346
|
+
this.streamer.destroy();
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
catch (err) {
|
|
2350
|
+
this.log.error({ err, msg: 'Failed to cleanup streamer', cid: this.id });
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Part of close(): destroys the sockets once and drops every socket and handler reference.
|
|
2356
|
+
*
|
|
2357
|
+
* @internal
|
|
2358
|
+
*/
|
|
2359
|
+
closeSockets() {
|
|
2360
|
+
// Socket teardown, in one documented order. Each stream owns and reports its own
|
|
2361
|
+
// lifecycle, so each is destroyed exactly once:
|
|
2362
|
+
// 1. the compression PassThrough (writeSocket), if compression replaced it
|
|
2363
|
+
// 2. the raw socket, which is also writeSocket when compression is not active
|
|
2364
|
+
// The compression streams themselves were destroyed in closeStreams().
|
|
2365
|
+
if (this.writeSocket && this.writeSocket !== this.socket && !this.writeSocket.destroyed) {
|
|
2366
|
+
try {
|
|
2367
|
+
this.writeSocket.destroy();
|
|
2368
|
+
}
|
|
2369
|
+
catch (err) {
|
|
2370
|
+
this.log.error({ err, cid: this.id });
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
if (this.socket && !this.socket.destroyed) {
|
|
2374
|
+
try {
|
|
2375
|
+
this.socket.destroy();
|
|
2376
|
+
}
|
|
2377
|
+
catch (err) {
|
|
2378
|
+
this.log.error({ err, cid: this.id });
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
// Null out all socket and handler references so the GC can collect
|
|
2382
|
+
// them even if the ImapFlow instance itself is still referenced.
|
|
2383
|
+
this.socket = null;
|
|
2384
|
+
this.writeSocket = null;
|
|
2385
|
+
// closeStreams() leaves these set when destroying them failed
|
|
2386
|
+
this._inflate = null;
|
|
2387
|
+
this._deflate = null;
|
|
2388
|
+
this._streamerErrorHandler = null;
|
|
2389
|
+
this._connectErrorHandler = null;
|
|
2390
|
+
this._socketError = null;
|
|
2391
|
+
this._socketClose = null;
|
|
2392
|
+
this._socketEnd = null;
|
|
2393
|
+
this._socketTimeout = null;
|
|
2394
|
+
}
|
|
2338
2395
|
/**
|
|
2339
2396
|
* Returns current quota
|
|
2340
2397
|
*
|
|
@@ -2492,7 +2549,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2492
2549
|
*
|
|
2493
2550
|
* @param path mailbox path to check for (unicode string). If value is an array then it is joined using current delimiter symbols. Namespace prefix is added automatically if required.
|
|
2494
2551
|
* @param query defines requested status items
|
|
2495
|
-
* @returns status of the indicated mailbox
|
|
2552
|
+
* @returns status of the indicated mailbox, or `false` if the server rejected the request
|
|
2496
2553
|
*
|
|
2497
2554
|
* @example
|
|
2498
2555
|
* let status = await client.status('INBOX', {unseen: true});
|
|
@@ -2507,7 +2564,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2507
2564
|
* otherwise IDLE is started by default on connection inactivity. NB! If `idle()` is called manually then it does not
|
|
2508
2565
|
* return until IDLE is finished which means you would have to call some other command out of scope.
|
|
2509
2566
|
*
|
|
2510
|
-
* @returns
|
|
2567
|
+
* @returns `false` if IDLE failed, `undefined` otherwise
|
|
2511
2568
|
*
|
|
2512
2569
|
* @example
|
|
2513
2570
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
@@ -2946,433 +3003,13 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2946
3003
|
* @example
|
|
2947
3004
|
* let mailbox = await client.mailboxOpen('INBOX');
|
|
2948
3005
|
* // download body part nr '1.2' from latest message
|
|
2949
|
-
* let
|
|
2950
|
-
*
|
|
3006
|
+
* let download = await client.download('*', '1.2');
|
|
3007
|
+
* if (download.content) {
|
|
3008
|
+
* download.content.pipe(fs.createWriteStream(download.meta.filename));
|
|
3009
|
+
* }
|
|
2951
3010
|
*/
|
|
2952
3011
|
async download(range, part, options) {
|
|
2953
|
-
|
|
2954
|
-
// no mailbox selected, nothing to do
|
|
2955
|
-
return {};
|
|
2956
|
-
}
|
|
2957
|
-
let downloadOptions = Object.assign({
|
|
2958
|
-
chunkSize: 64 * 1024,
|
|
2959
|
-
maxBytes: Infinity
|
|
2960
|
-
}, options || {});
|
|
2961
|
-
let hasMore = true;
|
|
2962
|
-
let processed = 0;
|
|
2963
|
-
let chunkSize = Number(downloadOptions.chunkSize) || 64 * 1024;
|
|
2964
|
-
// Normalized once here so every bounded stage of the pipeline below agrees on the budget
|
|
2965
|
-
let maxBytes = (0, limited_passthrough_js_1.normalizeByteLimit)(downloadOptions.maxBytes);
|
|
2966
|
-
let uid = false;
|
|
2967
|
-
if (part === '1') {
|
|
2968
|
-
// Special handling for part "1": in single-node emails (no childNodes),
|
|
2969
|
-
// the body is accessed via "TEXT" rather than "1", and headers via
|
|
2970
|
-
// "HEADER" instead of "1.MIME". Check bodyStructure to detect this.
|
|
2971
|
-
let response = await this.fetchOne(range, { uid: true, bodyStructure: true }, downloadOptions);
|
|
2972
|
-
if (!response) {
|
|
2973
|
-
return { response: false, chunk: false };
|
|
2974
|
-
}
|
|
2975
|
-
if (!uid && response.uid) {
|
|
2976
|
-
uid = response.uid;
|
|
2977
|
-
// force UID from now on even if first range was a sequence number
|
|
2978
|
-
range = uid;
|
|
2979
|
-
downloadOptions.uid = true;
|
|
2980
|
-
}
|
|
2981
|
-
if (!response.bodyStructure.childNodes) {
|
|
2982
|
-
// single text message
|
|
2983
|
-
part = 'TEXT';
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
let getNextPart = async (query) => {
|
|
2987
|
-
query = query || {};
|
|
2988
|
-
let mimeKey;
|
|
2989
|
-
if (!part) {
|
|
2990
|
-
query.source = {
|
|
2991
|
-
start: processed,
|
|
2992
|
-
maxLength: chunkSize
|
|
2993
|
-
};
|
|
2994
|
-
}
|
|
2995
|
-
else {
|
|
2996
|
-
part = part.toString().toLowerCase().trim();
|
|
2997
|
-
if (!query.bodyParts) {
|
|
2998
|
-
query.bodyParts = [];
|
|
2999
|
-
}
|
|
3000
|
-
if (query.size) {
|
|
3001
|
-
if (/^[\d.]+$/.test(part)) {
|
|
3002
|
-
// fetch meta as well
|
|
3003
|
-
mimeKey = part + '.mime';
|
|
3004
|
-
query.bodyParts.push(mimeKey);
|
|
3005
|
-
}
|
|
3006
|
-
else if (part === 'text') {
|
|
3007
|
-
mimeKey = 'header';
|
|
3008
|
-
query.bodyParts.push(mimeKey);
|
|
3009
|
-
}
|
|
3010
|
-
}
|
|
3011
|
-
query.bodyParts.push({
|
|
3012
|
-
key: part,
|
|
3013
|
-
start: processed,
|
|
3014
|
-
maxLength: chunkSize
|
|
3015
|
-
});
|
|
3016
|
-
}
|
|
3017
|
-
let response = await this.fetchOne(range, query, downloadOptions);
|
|
3018
|
-
if (!response) {
|
|
3019
|
-
return { response: false, chunk: false };
|
|
3020
|
-
}
|
|
3021
|
-
if (!uid && response.uid) {
|
|
3022
|
-
uid = response.uid;
|
|
3023
|
-
// force UID from now on even if first range was a sequence number
|
|
3024
|
-
range = uid;
|
|
3025
|
-
downloadOptions.uid = true;
|
|
3026
|
-
}
|
|
3027
|
-
let chunk = !part ? response.source : response.bodyParts && response.bodyParts.get(part);
|
|
3028
|
-
if (!chunk) {
|
|
3029
|
-
return {};
|
|
3030
|
-
}
|
|
3031
|
-
processed += chunk.length;
|
|
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
|
-
}
|
|
3048
|
-
let result = { chunk };
|
|
3049
|
-
if (query.size) {
|
|
3050
|
-
result.response = response;
|
|
3051
|
-
}
|
|
3052
|
-
if (query.bodyParts) {
|
|
3053
|
-
if (mimeKey === 'header') {
|
|
3054
|
-
result.mime = response.headers;
|
|
3055
|
-
}
|
|
3056
|
-
else {
|
|
3057
|
-
result.mime = response.bodyParts && mimeKey ? response.bodyParts.get(mimeKey) : undefined;
|
|
3058
|
-
}
|
|
3059
|
-
}
|
|
3060
|
-
return result;
|
|
3061
|
-
};
|
|
3062
|
-
let { response, chunk, mime } = await getNextPart({
|
|
3063
|
-
size: true,
|
|
3064
|
-
uid: true
|
|
3065
|
-
});
|
|
3066
|
-
if (!response || !chunk) {
|
|
3067
|
-
// ???
|
|
3068
|
-
return {};
|
|
3069
|
-
}
|
|
3070
|
-
let meta = {
|
|
3071
|
-
expectedSize: response.size
|
|
3072
|
-
};
|
|
3073
|
-
if (!part) {
|
|
3074
|
-
meta.contentType = 'message/rfc822';
|
|
3075
|
-
}
|
|
3076
|
-
else if (mime) {
|
|
3077
|
-
let headers = new mailsplit_1.Headers(mime);
|
|
3078
|
-
let contentType = libmime_1.default.parseHeaderValue(headers.getFirst('Content-Type'));
|
|
3079
|
-
let transferEncoding = libmime_1.default.parseHeaderValue(headers.getFirst('Content-Transfer-Encoding'));
|
|
3080
|
-
let disposition = libmime_1.default.parseHeaderValue(headers.getFirst('Content-Disposition'));
|
|
3081
|
-
if (contentType.value.toLowerCase().trim()) {
|
|
3082
|
-
meta.contentType = contentType.value.toLowerCase().trim();
|
|
3083
|
-
}
|
|
3084
|
-
if (contentType.params.charset) {
|
|
3085
|
-
meta.charset = contentType.params.charset.toLowerCase().trim();
|
|
3086
|
-
}
|
|
3087
|
-
if (transferEncoding.value) {
|
|
3088
|
-
meta.encoding = transferEncoding.value
|
|
3089
|
-
.replace(/\(.*\)/g, '')
|
|
3090
|
-
.toLowerCase()
|
|
3091
|
-
.trim();
|
|
3092
|
-
}
|
|
3093
|
-
if (disposition.value) {
|
|
3094
|
-
/* c8 ignore next */ // a parsed disposition value is never all-whitespace, so the `false` fallback is unreachable
|
|
3095
|
-
meta.disposition = disposition.value.toLowerCase().trim() || false;
|
|
3096
|
-
try {
|
|
3097
|
-
meta.disposition = libmime_1.default.decodeWords(meta.disposition);
|
|
3098
|
-
}
|
|
3099
|
-
catch {
|
|
3100
|
-
// failed to parse disposition, keep as is (most probably an unknown charset is used)
|
|
3101
|
-
}
|
|
3102
|
-
}
|
|
3103
|
-
if (contentType.params.format && contentType.params.format.toLowerCase().trim() === 'flowed') {
|
|
3104
|
-
meta.flowed = true;
|
|
3105
|
-
if (contentType.params.delsp && contentType.params.delsp.toLowerCase().trim() === 'yes') {
|
|
3106
|
-
meta.delSp = true;
|
|
3107
|
-
}
|
|
3108
|
-
}
|
|
3109
|
-
let filename = disposition.params.filename || contentType.params.name || false;
|
|
3110
|
-
if (filename) {
|
|
3111
|
-
try {
|
|
3112
|
-
filename = libmime_1.default.decodeWords(filename);
|
|
3113
|
-
}
|
|
3114
|
-
catch {
|
|
3115
|
-
// failed to parse filename, keep as is (most probably an unknown charset is used)
|
|
3116
|
-
}
|
|
3117
|
-
meta.filename = filename;
|
|
3118
|
-
}
|
|
3119
|
-
}
|
|
3120
|
-
let stream;
|
|
3121
|
-
let output;
|
|
3122
|
-
let fetchAborted = false;
|
|
3123
|
-
// Build a decoder pipeline that progressively transforms the raw FETCH data:
|
|
3124
|
-
// 1. Transfer-encoding decoder (base64 or quoted-printable -> binary)
|
|
3125
|
-
// 2. Format decoder (format=flowed -> plain text, if applicable)
|
|
3126
|
-
// 3. Charset decoder (non-UTF-8 -> UTF-8, for text parts only)
|
|
3127
|
-
// 4. Byte limiter (enforces maxBytes cap)
|
|
3128
|
-
// `stream` is the head of the pipeline (where raw chunks are written),
|
|
3129
|
-
// `output` is the tail (what the caller reads from).
|
|
3130
|
-
// Parts that arrived via FETCH BINARY (response.binaryParts) are already
|
|
3131
|
-
// decoded by the server - decoding again would corrupt the data, so stage 1
|
|
3132
|
-
// is skipped for them.
|
|
3133
|
-
let clientEncoding = response.binaryParts && part && response.binaryParts.has(part) ? false : meta.encoding;
|
|
3134
|
-
switch (clientEncoding) {
|
|
3135
|
-
case 'base64':
|
|
3136
|
-
output = stream = new libbase64_1.default.Decoder();
|
|
3137
|
-
break;
|
|
3138
|
-
case 'quoted-printable':
|
|
3139
|
-
output = stream = new libqp_1.default.Decoder();
|
|
3140
|
-
break;
|
|
3141
|
-
default:
|
|
3142
|
-
output = stream = new node_stream_1.PassThrough();
|
|
3143
|
-
}
|
|
3144
|
-
// Every byte-bounded stage of the pipeline. The fetch loop below stops as soon as any of
|
|
3145
|
-
// them has taken all it will accept. The limiter at the tail is not enough on its own: a
|
|
3146
|
-
// transform in the middle that buffers its whole input before emitting anything (the
|
|
3147
|
-
// format=flowed decoder, the Japanese charset decoder) leaves the tail limiter reporting
|
|
3148
|
-
// `limited === false` however much the server sends, so a download with a small maxBytes
|
|
3149
|
-
// would still pull the entire part off the wire.
|
|
3150
|
-
let limiters = [];
|
|
3151
|
-
let isLimited = () => limiters.some(entry => entry.limited);
|
|
3152
|
-
// Appending a stage means forwarding the current tail's errors to it before piping, so a
|
|
3153
|
-
// failure anywhere reaches the stream the caller is reading
|
|
3154
|
-
let pipeStage = (stage) => {
|
|
3155
|
-
output.on('error', err => {
|
|
3156
|
-
stage.emit('error', err);
|
|
3157
|
-
});
|
|
3158
|
-
output = output.pipe(stage);
|
|
3159
|
-
return stage;
|
|
3160
|
-
};
|
|
3161
|
-
let isTextNode = ['text/html', 'text/plain', 'text/x-amp-html'].includes(meta.contentType) || (part === '1' && !meta.contentType);
|
|
3162
|
-
if ((!meta.disposition || meta.disposition === 'inline') && isTextNode) {
|
|
3163
|
-
// RFC 3676 format=flowed text: unwrap soft line breaks
|
|
3164
|
-
if (meta.flowed) {
|
|
3165
|
-
// FlowedDecoder buffers its whole input before emitting, and being third party it
|
|
3166
|
-
// carries no bound of its own, so bound what it can ever be handed. Unwrapping only
|
|
3167
|
-
// removes bytes, so capping its input at maxBytes cannot push the delivered output
|
|
3168
|
-
// above the cap either.
|
|
3169
|
-
limiters.push(pipeStage(new limited_passthrough_js_1.LimitedPassthrough({ maxBytes })));
|
|
3170
|
-
pipeStage(new flowed_decoder_js_1.default(meta.delSp ? { delSp: true } : {}));
|
|
3171
|
-
}
|
|
3172
|
-
// Convert non-UTF-8 charsets to UTF-8 via a streaming decoder.
|
|
3173
|
-
// ASCII and UTF-8 need no conversion. Unknown charsets are left as-is.
|
|
3174
|
-
if (meta.charset && !['ascii', 'usascii', 'utf8'].includes(meta.charset.toLowerCase().replace(/[^a-z0-9]+/g, ''))) {
|
|
3175
|
-
try {
|
|
3176
|
-
let decoder = (0, tools_js_1.getDecoder)(meta.charset, maxBytes);
|
|
3177
|
-
// Safety listener attached first so the decoder always has at least
|
|
3178
|
-
// one 'error' listener. Prevents Node.js from throwing
|
|
3179
|
-
// ERR_UNHANDLED_ERROR if a later pipe setup step throws and leaves
|
|
3180
|
-
// the source-forwarding closure attached without a downstream
|
|
3181
|
-
// listener wired up. Any real listener the caller attaches still
|
|
3182
|
-
// fires in addition to this one.
|
|
3183
|
-
decoder.on('error', err => {
|
|
3184
|
-
this.log.warn({ err, charset: meta.charset, cid: this.id });
|
|
3185
|
-
});
|
|
3186
|
-
// The Japanese decoder buffers its whole input as well, and reports the same
|
|
3187
|
-
// `limited` flag the limiters do so the fetch loop can stop once it is full.
|
|
3188
|
-
// A streaming decoder has no such flag, which reads as false and is correct.
|
|
3189
|
-
limiters.push(pipeStage(decoder));
|
|
3190
|
-
// force to utf-8 for output
|
|
3191
|
-
meta.charset = 'utf-8';
|
|
3192
|
-
}
|
|
3193
|
-
catch {
|
|
3194
|
-
// do not decode charset
|
|
3195
|
-
}
|
|
3196
|
-
}
|
|
3197
|
-
}
|
|
3198
|
-
let limiter = pipeStage(new limited_passthrough_js_1.LimitedPassthrough({ maxBytes }));
|
|
3199
|
-
limiters.push(limiter);
|
|
3200
|
-
// Cleanup function
|
|
3201
|
-
const cleanup = () => {
|
|
3202
|
-
fetchAborted = true;
|
|
3203
|
-
if (stream && !stream.destroyed) {
|
|
3204
|
-
stream.destroy();
|
|
3205
|
-
}
|
|
3206
|
-
};
|
|
3207
|
-
// Listen for stream destruction
|
|
3208
|
-
output.once('error', cleanup);
|
|
3209
|
-
output.once('close', cleanup);
|
|
3210
|
-
let writeChunk = (chunk) => {
|
|
3211
|
-
if (isLimited() || fetchAborted || stream.destroyed) {
|
|
3212
|
-
return true;
|
|
3213
|
-
}
|
|
3214
|
-
return stream.write(chunk);
|
|
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);
|
|
3224
|
-
// Fetch remaining chunks in a loop, writing each to the decoder 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.
|
|
3228
|
-
let fetchAllParts = async () => {
|
|
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
|
-
}
|
|
3240
|
-
let { chunk } = await getNextPart();
|
|
3241
|
-
if (!chunk || fetchAborted) {
|
|
3242
|
-
break;
|
|
3243
|
-
}
|
|
3244
|
-
// Handle backpressure
|
|
3245
|
-
if (writeChunk(chunk) === false) {
|
|
3246
|
-
// Wait for drain event before continuing
|
|
3247
|
-
try {
|
|
3248
|
-
await new Promise((resolve, reject) => {
|
|
3249
|
-
// finish() is the listener itself, as settle() is for the TLS upgrade:
|
|
3250
|
-
// 'drain' and 'close' emit no arguments, 'error' emits the error, and
|
|
3251
|
-
// removal needs no separate handler references. It removes only the
|
|
3252
|
-
// three listeners this wait installed - removeAllListeners('error')
|
|
3253
|
-
// also took off the forwarder pipeStage() attached to the head stream
|
|
3254
|
-
// when the pipeline was built, and the head must keep that forwarder
|
|
3255
|
-
// for the life of the download or a chunk failure has nowhere to go.
|
|
3256
|
-
const finish = (err) => {
|
|
3257
|
-
for (let event of ['drain', 'error', 'close']) {
|
|
3258
|
-
stream.removeListener(event, finish);
|
|
3259
|
-
}
|
|
3260
|
-
/* c8 ignore next 2 */ // stream error during a backpressure drain wait is timing-dependent
|
|
3261
|
-
if (err) {
|
|
3262
|
-
reject(err);
|
|
3263
|
-
}
|
|
3264
|
-
else {
|
|
3265
|
-
resolve();
|
|
3266
|
-
}
|
|
3267
|
-
};
|
|
3268
|
-
stream.once('drain', finish);
|
|
3269
|
-
stream.once('error', finish);
|
|
3270
|
-
stream.once('close', finish);
|
|
3271
|
-
});
|
|
3272
|
-
/* c8 ignore start */ // re-throw path only triggers on a stream error mid-drain, which is timing-dependent
|
|
3273
|
-
}
|
|
3274
|
-
catch (err) {
|
|
3275
|
-
// Re-throw only if not aborted
|
|
3276
|
-
if (!fetchAborted) {
|
|
3277
|
-
throw err;
|
|
3278
|
-
}
|
|
3279
|
-
}
|
|
3280
|
-
/* c8 ignore stop */
|
|
3281
|
-
// Check if we should abort after waiting
|
|
3282
|
-
if (fetchAborted) {
|
|
3283
|
-
break;
|
|
3284
|
-
}
|
|
3285
|
-
}
|
|
3286
|
-
}
|
|
3287
|
-
};
|
|
3288
|
-
// A download is a sequence of chunk FETCHes with a backpressure wait in between. Those
|
|
3289
|
-
// gaps look exactly like an inactive connection, so without this auto-IDLE would start
|
|
3290
|
-
// between chunks and the next chunk would have to break it again - two extra round
|
|
3291
|
-
// trips per chunk, for as long as the consumer is slow. Counted before control returns
|
|
3292
|
-
// to the event loop: the head chunk's own FETCH already armed the auto-IDLE timer, and
|
|
3293
|
-
// with a very short autoIdleDelay that timer could otherwise fire before the deferred
|
|
3294
|
-
// chunk loop below has marked the download open.
|
|
3295
|
-
this._openDownloads++;
|
|
3296
|
-
let downloadDone = false;
|
|
3297
|
-
let finishDownload = () => {
|
|
3298
|
-
if (!downloadDone) {
|
|
3299
|
-
downloadDone = true;
|
|
3300
|
-
this._openDownloads--;
|
|
3301
|
-
this.autoidle();
|
|
3302
|
-
}
|
|
3303
|
-
};
|
|
3304
|
-
// Kick off the download pipeline asynchronously. The first chunk was
|
|
3305
|
-
// already fetched above (to get metadata); write it to the decoder
|
|
3306
|
-
// stream and then fetch remaining chunks via fetchAllParts().
|
|
3307
|
-
// setImmediate ensures the caller gets the {meta, content} return
|
|
3308
|
-
// value before streaming begins.
|
|
3309
|
-
let runFetchAllParts = () => {
|
|
3310
|
-
fetchAllParts()
|
|
3311
|
-
.catch(err => {
|
|
3312
|
-
if (!fetchAborted && stream && !stream.destroyed) {
|
|
3313
|
-
stream.emit('error', err);
|
|
3314
|
-
/* c8 ignore start */ // the else logs when a fetch error arrives after the stream was already torn down (timing-dependent)
|
|
3315
|
-
}
|
|
3316
|
-
else {
|
|
3317
|
-
// Log when error cannot be emitted to stream
|
|
3318
|
-
this.log.warn({
|
|
3319
|
-
msg: 'Download error after stream closed',
|
|
3320
|
-
err,
|
|
3321
|
-
fetchAborted,
|
|
3322
|
-
streamDestroyed: stream?.destroyed,
|
|
3323
|
-
cid: this.id
|
|
3324
|
-
});
|
|
3325
|
-
}
|
|
3326
|
-
/* c8 ignore stop */
|
|
3327
|
-
})
|
|
3328
|
-
.finally(() => {
|
|
3329
|
-
finishDownload();
|
|
3330
|
-
if (!fetchAborted && stream && !stream.destroyed) {
|
|
3331
|
-
stream.end();
|
|
3332
|
-
}
|
|
3333
|
-
})
|
|
3334
|
-
// Terminal guard: nothing consumes this chain, so a throw from either handler
|
|
3335
|
-
// above rejects a promise nobody holds and takes the process down on
|
|
3336
|
-
// unhandledRejection. Reaching it always means an invariant broke - the head
|
|
3337
|
-
// stream kept pipeStage()'s error forwarder for the life of the download, so
|
|
3338
|
-
// emit('error') above has somewhere to go - which is why it logs at error even
|
|
3339
|
-
// for a routine-looking connection code.
|
|
3340
|
-
.catch(err => this.log.error({ msg: 'Failed to fail the download stream', err, cid: this.id }));
|
|
3341
|
-
};
|
|
3342
|
-
setImmediate(() => {
|
|
3343
|
-
let writeResult;
|
|
3344
|
-
try {
|
|
3345
|
-
writeResult = writeChunk(chunk);
|
|
3346
|
-
}
|
|
3347
|
-
catch (err) {
|
|
3348
|
-
stream.emit('error', err);
|
|
3349
|
-
finishDownload();
|
|
3350
|
-
/* c8 ignore next 3 */ // emitting the error above triggers cleanup (fetchAborted=true), so this end() guard is already false here
|
|
3351
|
-
if (!fetchAborted && stream && !stream.destroyed) {
|
|
3352
|
-
stream.end();
|
|
3353
|
-
}
|
|
3354
|
-
return;
|
|
3355
|
-
}
|
|
3356
|
-
/* c8 ignore next 9 */ // `stream` is piped to the limiter before this runs, so the head write drains synchronously and always returns true (verified for chunkSize up to 8MB); the drain-wait branch is unreachable
|
|
3357
|
-
if (!writeResult) {
|
|
3358
|
-
// Initial chunk filled the buffer, wait for drain
|
|
3359
|
-
stream.once('drain', () => {
|
|
3360
|
-
if (!fetchAborted) {
|
|
3361
|
-
runFetchAllParts();
|
|
3362
|
-
}
|
|
3363
|
-
else {
|
|
3364
|
-
finishDownload();
|
|
3365
|
-
}
|
|
3366
|
-
});
|
|
3367
|
-
}
|
|
3368
|
-
else {
|
|
3369
|
-
runFetchAllParts();
|
|
3370
|
-
}
|
|
3371
|
-
});
|
|
3372
|
-
return {
|
|
3373
|
-
meta,
|
|
3374
|
-
content: output
|
|
3375
|
-
};
|
|
3012
|
+
return await (0, download_js_1.downloadMessage)(this, range, part, options);
|
|
3376
3013
|
}
|
|
3377
3014
|
/**
|
|
3378
3015
|
* Fetch multiple attachments as Buffer values
|
|
@@ -3390,119 +3027,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3390
3027
|
* process.stdout.write(response[3].content)
|
|
3391
3028
|
*/
|
|
3392
3029
|
async downloadMany(range, parts, options) {
|
|
3393
|
-
|
|
3394
|
-
// no mailbox selected, nothing to do
|
|
3395
|
-
return {};
|
|
3396
|
-
}
|
|
3397
|
-
let downloadOptions = Object.assign({
|
|
3398
|
-
chunkSize: 64 * 1024,
|
|
3399
|
-
maxBytes: Infinity
|
|
3400
|
-
}, options || {});
|
|
3401
|
-
let query = { bodyParts: [] };
|
|
3402
|
-
for (let part of parts) {
|
|
3403
|
-
query.bodyParts.push(part + '.mime');
|
|
3404
|
-
query.bodyParts.push(part);
|
|
3405
|
-
}
|
|
3406
|
-
let response = await this.fetchOne(range, query, downloadOptions);
|
|
3407
|
-
if (!response || !response.bodyParts) {
|
|
3408
|
-
return { response: false };
|
|
3409
|
-
}
|
|
3410
|
-
let data = {};
|
|
3411
|
-
for (let [part, content] of response.bodyParts) {
|
|
3412
|
-
let keyParts = part.split('.mime');
|
|
3413
|
-
// The server chooses the BODY[...] keys it answers with: never let one be a
|
|
3414
|
-
// prototype-chain name, or the assignments below write onto Object.prototype
|
|
3415
|
-
// (process-wide pollution) instead of the result object.
|
|
3416
|
-
if ((0, tools_js_1.isUnsafeKey)(keyParts[0])) {
|
|
3417
|
-
continue;
|
|
3418
|
-
}
|
|
3419
|
-
if (keyParts.length === 1) {
|
|
3420
|
-
// content
|
|
3421
|
-
let key = keyParts[0];
|
|
3422
|
-
if (!data[key]) {
|
|
3423
|
-
data[key] = { content };
|
|
3424
|
-
}
|
|
3425
|
-
else {
|
|
3426
|
-
data[key].content = content;
|
|
3427
|
-
}
|
|
3428
|
-
}
|
|
3429
|
-
else if (keyParts.length === 2) {
|
|
3430
|
-
// header
|
|
3431
|
-
let key = keyParts[0];
|
|
3432
|
-
if (!data[key]) {
|
|
3433
|
-
data[key] = {};
|
|
3434
|
-
}
|
|
3435
|
-
let entry = data[key];
|
|
3436
|
-
if (!entry.meta) {
|
|
3437
|
-
entry.meta = {};
|
|
3438
|
-
}
|
|
3439
|
-
let meta = entry.meta;
|
|
3440
|
-
let headers = new mailsplit_1.Headers(content);
|
|
3441
|
-
let contentType = libmime_1.default.parseHeaderValue(headers.getFirst('Content-Type'));
|
|
3442
|
-
let transferEncoding = libmime_1.default.parseHeaderValue(headers.getFirst('Content-Transfer-Encoding'));
|
|
3443
|
-
let disposition = libmime_1.default.parseHeaderValue(headers.getFirst('Content-Disposition'));
|
|
3444
|
-
if (contentType.value.toLowerCase().trim()) {
|
|
3445
|
-
meta.contentType = contentType.value.toLowerCase().trim();
|
|
3446
|
-
}
|
|
3447
|
-
if (contentType.params.charset) {
|
|
3448
|
-
meta.charset = contentType.params.charset.toLowerCase().trim();
|
|
3449
|
-
}
|
|
3450
|
-
if (transferEncoding.value) {
|
|
3451
|
-
meta.encoding = transferEncoding.value
|
|
3452
|
-
.replace(/\(.*\)/g, '')
|
|
3453
|
-
.toLowerCase()
|
|
3454
|
-
.trim();
|
|
3455
|
-
}
|
|
3456
|
-
if (disposition.value) {
|
|
3457
|
-
/* c8 ignore next */ // a parsed disposition value is never all-whitespace, so the `false` fallback is unreachable
|
|
3458
|
-
meta.disposition = disposition.value.toLowerCase().trim() || false;
|
|
3459
|
-
try {
|
|
3460
|
-
meta.disposition = libmime_1.default.decodeWords(meta.disposition);
|
|
3461
|
-
}
|
|
3462
|
-
catch {
|
|
3463
|
-
// failed to parse disposition, keep as is (most probably an unknown charset is used)
|
|
3464
|
-
}
|
|
3465
|
-
}
|
|
3466
|
-
if (contentType.params.format && contentType.params.format.toLowerCase().trim() === 'flowed') {
|
|
3467
|
-
meta.flowed = true;
|
|
3468
|
-
if (contentType.params.delsp && contentType.params.delsp.toLowerCase().trim() === 'yes') {
|
|
3469
|
-
meta.delSp = true;
|
|
3470
|
-
}
|
|
3471
|
-
}
|
|
3472
|
-
let filename = disposition.params.filename || contentType.params.name || false;
|
|
3473
|
-
if (filename) {
|
|
3474
|
-
try {
|
|
3475
|
-
filename = libmime_1.default.decodeWords(filename);
|
|
3476
|
-
}
|
|
3477
|
-
catch {
|
|
3478
|
-
// failed to parse filename, keep as is (most probably an unknown charset is used)
|
|
3479
|
-
}
|
|
3480
|
-
meta.filename = filename;
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
}
|
|
3484
|
-
for (let part of Object.keys(data)) {
|
|
3485
|
-
let entry = data[part];
|
|
3486
|
-
// `meta` is only built from the companion BODY[<part>.MIME] item. A server may
|
|
3487
|
-
// legally answer with fewer items than were requested, and one part arriving
|
|
3488
|
-
// without its MIME headers must not cost the caller the whole download.
|
|
3489
|
-
let meta = entry.meta || {};
|
|
3490
|
-
entry.meta = meta;
|
|
3491
|
-
// parts that arrived via FETCH BINARY (response.binaryParts) are already
|
|
3492
|
-
// decoded by the server - decoding again would corrupt the data
|
|
3493
|
-
let clientEncoding = response.binaryParts && response.binaryParts.has(part) ? false : meta.encoding;
|
|
3494
|
-
switch (clientEncoding) {
|
|
3495
|
-
case 'base64':
|
|
3496
|
-
entry.content = entry.content ? libbase64_1.default.decode(entry.content.toString()) : null;
|
|
3497
|
-
break;
|
|
3498
|
-
case 'quoted-printable':
|
|
3499
|
-
entry.content = entry.content ? libqp_1.default.decode(entry.content.toString()) : null;
|
|
3500
|
-
break;
|
|
3501
|
-
default:
|
|
3502
|
-
// keep as is, already a buffer
|
|
3503
|
-
}
|
|
3504
|
-
}
|
|
3505
|
-
return data;
|
|
3030
|
+
return await (0, download_js_1.downloadMessageParts)(this, range, parts, options);
|
|
3506
3031
|
}
|
|
3507
3032
|
/** @internal */
|
|
3508
3033
|
async run(command, ...args) {
|
|
@@ -3872,7 +3397,8 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3872
3397
|
}
|
|
3873
3398
|
/**
|
|
3874
3399
|
* Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
|
|
3875
|
-
* (e.g., STARTTLS) or transferring socket ownership.
|
|
3400
|
+
* (e.g., STARTTLS) or transferring socket ownership. Call it while the connection is not
|
|
3401
|
+
* idling: an IDLE in progress is not broken first, so the server still expects `DONE`.
|
|
3876
3402
|
*
|
|
3877
3403
|
* @returns Socket objects: `readSocket` is the read socket (inflated socket if compression is enabled, raw socket otherwise),
|
|
3878
3404
|
* `writeSocket` the write socket and `socket` the raw underlying socket (same as readSocket/writeSocket when compression is disabled)
|
|
@@ -3887,6 +3413,11 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3887
3413
|
// compression is active, the PassThrough writeSocket - so the connection
|
|
3888
3414
|
// is fully released to the caller.
|
|
3889
3415
|
this.clearSocketHandlers();
|
|
3416
|
+
// The socket now belongs to the caller. Marking the client unusable keeps
|
|
3417
|
+
// auto-IDLE (armed now, or re-armed by a lock release or a finished download)
|
|
3418
|
+
// and a later dispose from writing IDLE or LOGOUT onto it.
|
|
3419
|
+
this.usable = false;
|
|
3420
|
+
(0, tools_js_1.clearTimer)(this.idleStartTimer);
|
|
3890
3421
|
const readSocket = this._inflate || socket;
|
|
3891
3422
|
const writeSocket = this.writeSocket || socket;
|
|
3892
3423
|
// Defense-in-depth: when compression is active the raw socket is orphaned
|
|
@@ -3985,6 +3516,23 @@ exports.ImapFlow = ImapFlow;
|
|
|
3985
3516
|
* console.log(`${entry.cid} ${entry.msg}`);
|
|
3986
3517
|
* });
|
|
3987
3518
|
*/
|
|
3519
|
+
// Installed outside the class body: a computed `[Symbol.asyncDispose]` key would turn into a
|
|
3520
|
+
// method named "undefined" on Node.js 20.0-20.3, which predate the symbol
|
|
3521
|
+
if (typeof Symbol.asyncDispose === 'symbol') {
|
|
3522
|
+
ImapFlow.prototype[Symbol.asyncDispose] = async function () {
|
|
3523
|
+
if (this.usable) {
|
|
3524
|
+
try {
|
|
3525
|
+
await this.logout();
|
|
3526
|
+
}
|
|
3527
|
+
catch (err) {
|
|
3528
|
+
(0, tools_js_1.logConnectionError)(this, 'LOGOUT failed while disposing', err);
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
// logout() already closes the connection; close() is idempotent and covers a client
|
|
3532
|
+
// that never connected or whose logout was skipped
|
|
3533
|
+
this.close();
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3988
3536
|
// Both `import { ImapFlow } from 'imapflow'` and `import imapflow from 'imapflow'` work, the
|
|
3989
3537
|
// latter matching the shape `require('imapflow')` has always had
|
|
3990
3538
|
const imapflow = { ImapFlow, AuthenticationFailure: errors_js_1.AuthenticationFailure };
|