imapflow 2.2.5 → 2.2.7
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 +17 -0
- package/dist/cjs/commands/append.js +10 -3
- package/dist/cjs/commands/authenticate.js +27 -18
- package/dist/cjs/commands/close.d.ts +12 -1
- package/dist/cjs/commands/close.js +4 -2
- package/dist/cjs/commands/copyuid-parser.js +16 -2
- package/dist/cjs/commands/delete.js +2 -1
- package/dist/cjs/commands/esearch-parser.js +8 -2
- package/dist/cjs/commands/fetch.js +35 -9
- package/dist/cjs/commands/id.js +8 -1
- package/dist/cjs/commands/idle.js +15 -5
- package/dist/cjs/commands/list.js +10 -1
- package/dist/cjs/commands/login.js +5 -1
- package/dist/cjs/commands/logout.js +7 -0
- package/dist/cjs/commands/namespace.js +7 -3
- package/dist/cjs/commands/quota.js +3 -1
- package/dist/cjs/commands/rename.js +2 -1
- package/dist/cjs/commands/select.js +5 -0
- package/dist/cjs/commands/status.js +6 -1
- package/dist/cjs/commands/store.d.ts +1 -1
- package/dist/cjs/commands/store.js +1 -2
- package/dist/cjs/download.js +82 -91
- package/dist/cjs/handler/imap-compiler.js +45 -29
- package/dist/cjs/handler/limits.d.ts +11 -0
- package/dist/cjs/handler/limits.js +16 -1
- package/dist/cjs/handler/parser-instance.d.ts +10 -0
- package/dist/cjs/handler/parser-instance.js +25 -10
- package/dist/cjs/handler/token-parser.js +162 -78
- package/dist/cjs/imap-flow.js +235 -89
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/proxy-connection.js +7 -7
- package/dist/cjs/search-compiler.js +37 -16
- package/dist/cjs/special-use.js +10 -5
- package/dist/cjs/tools.d.ts +10 -1
- package/dist/cjs/tools.js +30 -3
- package/dist/cjs/types.d.ts +19 -4
- package/dist/esm/commands/append.js +11 -4
- package/dist/esm/commands/authenticate.js +28 -19
- package/dist/esm/commands/close.d.ts +12 -1
- package/dist/esm/commands/close.js +5 -3
- package/dist/esm/commands/copyuid-parser.js +16 -2
- package/dist/esm/commands/delete.js +2 -1
- package/dist/esm/commands/esearch-parser.js +8 -2
- package/dist/esm/commands/fetch.js +35 -9
- package/dist/esm/commands/id.js +9 -2
- package/dist/esm/commands/idle.js +16 -6
- package/dist/esm/commands/list.js +10 -1
- package/dist/esm/commands/login.js +6 -2
- package/dist/esm/commands/logout.js +7 -0
- package/dist/esm/commands/namespace.js +7 -3
- package/dist/esm/commands/quota.js +3 -1
- package/dist/esm/commands/rename.js +2 -1
- package/dist/esm/commands/select.js +5 -0
- package/dist/esm/commands/status.js +7 -2
- package/dist/esm/commands/store.d.ts +1 -1
- package/dist/esm/commands/store.js +1 -2
- package/dist/esm/download.js +82 -91
- package/dist/esm/handler/imap-compiler.js +45 -29
- package/dist/esm/handler/limits.d.ts +11 -0
- package/dist/esm/handler/limits.js +14 -0
- package/dist/esm/handler/parser-instance.d.ts +10 -0
- package/dist/esm/handler/parser-instance.js +25 -10
- package/dist/esm/handler/token-parser.js +163 -79
- package/dist/esm/imap-flow.js +235 -89
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/proxy-connection.js +7 -7
- package/dist/esm/search-compiler.js +37 -16
- package/dist/esm/special-use.js +10 -5
- package/dist/esm/tools.d.ts +10 -1
- package/dist/esm/tools.js +29 -3
- package/dist/esm/types.d.ts +19 -4
- package/package.json +3 -2
package/dist/cjs/imap-flow.js
CHANGED
|
@@ -260,6 +260,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
260
260
|
this.requestTagMap = new Map();
|
|
261
261
|
this.requestQueue = [];
|
|
262
262
|
this.currentRequest = false;
|
|
263
|
+
this.parkedRelease = null;
|
|
263
264
|
this._unknownTagCount = 0;
|
|
264
265
|
this._nextUnknownTagWarn = 1;
|
|
265
266
|
this.writeBytesCounter = 0;
|
|
@@ -289,6 +290,8 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
289
290
|
this.autoIdleDelay = normalizeAutoIdleDelay(this.options.autoIdleDelay, this.socketTimeout, this.log, this.id);
|
|
290
291
|
this._lastPollAt = 0;
|
|
291
292
|
this._openDownloads = 0;
|
|
293
|
+
this._recoveryPending = false;
|
|
294
|
+
this._processingResponse = false;
|
|
292
295
|
this.missingIdleCommand = (this.options.missingIdleCommand || '').toString().toUpperCase().trim() || 'NOOP';
|
|
293
296
|
this.disableBinary = !!this.options.disableBinary;
|
|
294
297
|
this.idHashAlgorithm = this.options.idHashAlgorithm || 'md5';
|
|
@@ -296,6 +299,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
296
299
|
this.skipListStatusArgs = false;
|
|
297
300
|
this.skipListAuxArgs = false;
|
|
298
301
|
this.skipLsub = false;
|
|
302
|
+
this.skipIdle = false;
|
|
299
303
|
this.skipRev2 = !!this.options.disableIMAP4rev2;
|
|
300
304
|
// Named error handler for proper cleanup. Certain error codes represent
|
|
301
305
|
// expected socket/network issues (buffer exhaustion, connection reset, broken pipe,
|
|
@@ -444,15 +448,11 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
444
448
|
/** @internal */
|
|
445
449
|
async send(data) {
|
|
446
450
|
if (this.state === this.states.LOGOUT) {
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
request.reject(this.createNoConnectionError(false, { rejectedFrom: 'sendAfterLogout', command: request.command }));
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return;
|
|
451
|
+
// Already logged out. Thrown rather than rejected here so trySend() clears the
|
|
452
|
+
// request from currentRequest and goes on to the next one: a request left there
|
|
453
|
+
// never gets a tagged response, and everything queued behind it would wait for
|
|
454
|
+
// the socket watchdog.
|
|
455
|
+
throw this.createNoConnectionError(false, { rejectedFrom: 'sendAfterLogout', command: data.command });
|
|
456
456
|
}
|
|
457
457
|
// Classify before the first await. Every frame of this command - the command line and
|
|
458
458
|
// any continuation write that follows it - belongs to it until the next send(), because
|
|
@@ -475,14 +475,16 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
475
475
|
literalMinus: (0, tools_js_1.hasCapability)(this, 'LITERAL-') || this.capabilities.has('LITERAL+')
|
|
476
476
|
});
|
|
477
477
|
this.commandParts = compiled;
|
|
478
|
-
// Compile again for logging with isLogging=true: masks sensitive values
|
|
479
|
-
// like passwords while producing a human-readable command string
|
|
480
|
-
let logCompiled = await (0, imap_handler_js_1.compiler)(data, {
|
|
481
|
-
isLogging: true
|
|
482
|
-
});
|
|
483
478
|
/* c8 ignore next */ // send() is always invoked with a request object carrying options, so the {} fallback is unreachable
|
|
484
479
|
let options = data.options || {};
|
|
485
|
-
|
|
480
|
+
// Compile again for logging with isLogging=true: masks sensitive values
|
|
481
|
+
// like passwords while producing a human-readable command string
|
|
482
|
+
if (this.isLogLevelEnabled('debug')) {
|
|
483
|
+
let logCompiled = await (0, imap_handler_js_1.compiler)(data, {
|
|
484
|
+
isLogging: true
|
|
485
|
+
});
|
|
486
|
+
this.log.debug({ src: 'c', msg: logCompiled.toString(), cid: this.id, comment: options.comment });
|
|
487
|
+
}
|
|
486
488
|
// Send the first part (command text). If there are literal parts,
|
|
487
489
|
// the server will respond with "+" continuations and reader() will
|
|
488
490
|
// send each remaining part from this.commandParts.
|
|
@@ -525,7 +527,6 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
525
527
|
// nothing reached the wire, so no tagged response ever clears it, and
|
|
526
528
|
// every later command would queue behind it until the socket timeout.
|
|
527
529
|
// Reject the failed command and keep draining the queue.
|
|
528
|
-
this.commandParts = [];
|
|
529
530
|
this.rejectCurrentRequest(err);
|
|
530
531
|
}
|
|
531
532
|
}
|
|
@@ -644,16 +645,53 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
644
645
|
// a way that leaves the command's outcome unknown.
|
|
645
646
|
/** @internal */
|
|
646
647
|
rejectCurrentRequest(err) {
|
|
648
|
+
let request = this.takeCurrentRequest();
|
|
649
|
+
if (request) {
|
|
650
|
+
request.reject(err);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Takes the in-flight command off the connection: its tag map entry, and the literal parts it
|
|
655
|
+
* still had to send (a tagged NO before the "+" of a refused APPEND leaves them behind, and
|
|
656
|
+
* they must neither stay referenced nor answer a later continuation). The parts are dropped
|
|
657
|
+
* whether or not a command is still current, as a failed dispatch may have cleared it already.
|
|
658
|
+
*
|
|
659
|
+
* @returns The pending request entry, if the command was still current
|
|
660
|
+
* @internal
|
|
661
|
+
*/
|
|
662
|
+
takeCurrentRequest() {
|
|
663
|
+
this.commandParts = [];
|
|
647
664
|
if (!this.currentRequest) {
|
|
648
|
-
return;
|
|
665
|
+
return undefined;
|
|
649
666
|
}
|
|
650
667
|
let tag = this.currentRequest.tag;
|
|
651
668
|
this.currentRequest = false;
|
|
652
669
|
let request = this.requestTagMap.get(tag);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
670
|
+
this.requestTagMap.delete(tag);
|
|
671
|
+
return request;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Resolves a command's promise with its tagged response and parks the reader until the
|
|
675
|
+
* command's handler calls next(). The release is kept in parkedRelease so a handler that
|
|
676
|
+
* throws before calling next() can be unparked (see releaseOrphanedResponse()); every path
|
|
677
|
+
* that resolves a command this way goes through here for that reason.
|
|
678
|
+
*
|
|
679
|
+
* @param request - Pending request entry
|
|
680
|
+
* @param parsed - Parsed tagged response
|
|
681
|
+
* @param hasTrailingData - Whether more input was already buffered after this line
|
|
682
|
+
* @internal
|
|
683
|
+
*/
|
|
684
|
+
resolveParked(request, parsed, hasTrailingData) {
|
|
685
|
+
return new Promise(resolve => {
|
|
686
|
+
let release = () => {
|
|
687
|
+
if (this.parkedRelease === release) {
|
|
688
|
+
this.parkedRelease = null;
|
|
689
|
+
}
|
|
690
|
+
resolve();
|
|
691
|
+
};
|
|
692
|
+
this.parkedRelease = release;
|
|
693
|
+
request.resolve({ response: parsed, next: release, hasTrailingData });
|
|
694
|
+
});
|
|
657
695
|
}
|
|
658
696
|
/**
|
|
659
697
|
* Waits out a throttle back-off.
|
|
@@ -687,6 +725,10 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
687
725
|
while ((data = this.streamer.read()) !== null) {
|
|
688
726
|
let keepReading;
|
|
689
727
|
try {
|
|
728
|
+
// The reader is parked here for as long as processing takes (a fetch consumer
|
|
729
|
+
// working on a row), so a socket that goes quiet meanwhile is this side's doing,
|
|
730
|
+
// see _socketTimeout
|
|
731
|
+
this._processingResponse = true;
|
|
690
732
|
keepReading = await this.handleResponse(data);
|
|
691
733
|
}
|
|
692
734
|
catch (err) {
|
|
@@ -703,6 +745,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
703
745
|
this.failProtocol(error);
|
|
704
746
|
}
|
|
705
747
|
finally {
|
|
748
|
+
this._processingResponse = false;
|
|
706
749
|
this.releaseStreamData(data);
|
|
707
750
|
}
|
|
708
751
|
if (!keepReading) {
|
|
@@ -794,15 +837,14 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
794
837
|
this.log.warn({ err, cid: this.id });
|
|
795
838
|
}
|
|
796
839
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
if (
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
this.log.debug({ src: 's', msg: logCompiled.toString(), cid: this.id, nullBytesRemoved: parsed.nullBytesRemoved });
|
|
840
|
+
// FETCH responses are the bulk of a session, so they log at trace. The serialization is
|
|
841
|
+
// skipped when the level is off.
|
|
842
|
+
let logLevel = /^\d+$/.test(parsed.command || '') && parsed.attributes && parsed.attributes[0] && parsed.attributes[0].value === 'FETCH' ? 'trace' : 'debug';
|
|
843
|
+
if (this.isLogLevelEnabled(logLevel)) {
|
|
844
|
+
let logCompiled = await (0, imap_handler_js_1.compiler)(parsed, {
|
|
845
|
+
isLogging: true
|
|
846
|
+
});
|
|
847
|
+
this.log[logLevel]({ src: 's', msg: logCompiled.toString(), cid: this.id, nullBytesRemoved: parsed.nullBytesRemoved });
|
|
806
848
|
}
|
|
807
849
|
// IMAP "+" (continuation request) handling. The server sends "+" in two cases:
|
|
808
850
|
// 1. During IDLE or AUTHENTICATE, where a custom handler (onPlusTag) processes it
|
|
@@ -878,9 +920,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
878
920
|
// and an entirely unknown tag is recorded but tolerated.
|
|
879
921
|
if (parsed.tag && !['*', '+'].includes(parsed.tag)) {
|
|
880
922
|
if (this.currentRequest && this.currentRequest.tag === parsed.tag && this.currentRequest.sent) {
|
|
881
|
-
let request = this.
|
|
882
|
-
this.requestTagMap.delete(parsed.tag);
|
|
883
|
-
this.currentRequest = false;
|
|
923
|
+
let request = this.takeCurrentRequest();
|
|
884
924
|
if (request) {
|
|
885
925
|
await this.settleRequest(request, parsed, !!data.trailingAfterLine);
|
|
886
926
|
}
|
|
@@ -940,7 +980,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
940
980
|
case 'BYE':
|
|
941
981
|
// hasTrailingData is forwarded so STARTTLS can detect a plaintext
|
|
942
982
|
// injection (data buffered after the tagged OK, before the handshake).
|
|
943
|
-
await
|
|
983
|
+
await this.resolveParked(request, parsed, hasTrailingData);
|
|
944
984
|
break;
|
|
945
985
|
case 'NO':
|
|
946
986
|
case 'BAD': {
|
|
@@ -968,7 +1008,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
968
1008
|
// is told nothing else about it, so this entry is the only record that
|
|
969
1009
|
// the response was truncated.
|
|
970
1010
|
this.log.warn({ msg: 'Partial FETCH response', cid: this.id, err });
|
|
971
|
-
await
|
|
1011
|
+
await this.resolveParked(request, parsed);
|
|
972
1012
|
break;
|
|
973
1013
|
}
|
|
974
1014
|
let throttleDelay = false;
|
|
@@ -1062,6 +1102,16 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1062
1102
|
if (typeof socket.setKeepAlive === 'function') {
|
|
1063
1103
|
socket.setKeepAlive(true, 5 * 1000);
|
|
1064
1104
|
}
|
|
1105
|
+
this.armSocketTimeout(socket);
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Arms the inactivity watchdog on a socket. Guarded the same way as the rest of
|
|
1109
|
+
* configureSocket(): other runtimes stub socket features.
|
|
1110
|
+
*
|
|
1111
|
+
* @param socket - The socket to arm
|
|
1112
|
+
* @internal
|
|
1113
|
+
*/
|
|
1114
|
+
armSocketTimeout(socket) {
|
|
1065
1115
|
if (typeof socket.setTimeout === 'function') {
|
|
1066
1116
|
socket.setTimeout(this.socketTimeout);
|
|
1067
1117
|
}
|
|
@@ -1101,25 +1151,50 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1101
1151
|
this._socketTimeout =
|
|
1102
1152
|
this._socketTimeout ||
|
|
1103
1153
|
(() => {
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
|
|
1154
|
+
const timeoutError = () => {
|
|
1155
|
+
const err = new Error('Socket timeout');
|
|
1156
|
+
err.code = 'ETIMEOUT';
|
|
1157
|
+
return err;
|
|
1158
|
+
};
|
|
1159
|
+
if (!this.usable || !this.socket || this.socket.destroyed) {
|
|
1160
|
+
this.emitError(timeoutError());
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (this._processingResponse) {
|
|
1164
|
+
// The reader is parked processing a response (a fetch consumer taking its
|
|
1165
|
+
// time over a row), so nothing is being read: the quiet is this side's, and
|
|
1166
|
+
// the server is not overdue with anything. The timer is one-shot and nothing
|
|
1167
|
+
// resumes it until reading does, so it is re-armed for the next check.
|
|
1168
|
+
this.log.debug({ msg: 'Socket timeout while a response is being processed', cid: this.id });
|
|
1169
|
+
this.armSocketTimeout(this.socket);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
// A throttle back-off is quiet by design, the NOOP below probes that the server
|
|
1173
|
+
// is still there
|
|
1174
|
+
const quietExpected = this.idling || this._openDownloads || this.currentLock || this._throttleWaits.size;
|
|
1107
1175
|
const commandStuck = this.currentRequest && !(this.idling && this.currentRequest.command === 'IDLE');
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1176
|
+
// A recovery NOOP that has not settled by the next timeout means the peer is gone
|
|
1177
|
+
if (quietExpected && !commandStuck && !this._recoveryPending) {
|
|
1178
|
+
// The inactivity timer is one-shot, re-armed only by traffic. The NOOP can not
|
|
1179
|
+
// reach the wire while IDLE still waits for its continuation (DONE is sent
|
|
1180
|
+
// only after the "+"), so without re-arming it here a peer that went silent
|
|
1181
|
+
// right after IDLE would never time out again.
|
|
1182
|
+
this._recoveryPending = true;
|
|
1183
|
+
this.armSocketTimeout(this.socket);
|
|
1184
|
+
this.run('NOOP')
|
|
1185
|
+
.catch(err => {
|
|
1114
1186
|
this.log.warn({ msg: 'Connection recovery failed after timeout', err, cid: this.id });
|
|
1115
1187
|
if (!this.isClosed) {
|
|
1116
1188
|
this.close();
|
|
1117
1189
|
}
|
|
1190
|
+
})
|
|
1191
|
+
.finally(() => {
|
|
1192
|
+
this._recoveryPending = false;
|
|
1118
1193
|
});
|
|
1119
1194
|
}
|
|
1120
1195
|
else {
|
|
1121
1196
|
this.log.debug({ msg: 'Socket timeout', cid: this.id });
|
|
1122
|
-
this.emitError(
|
|
1197
|
+
this.emitError(timeoutError());
|
|
1123
1198
|
}
|
|
1124
1199
|
});
|
|
1125
1200
|
const socket = this.socket;
|
|
@@ -1349,8 +1424,15 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1349
1424
|
err.tlsFailed = true;
|
|
1350
1425
|
throw err;
|
|
1351
1426
|
}
|
|
1352
|
-
// Opportunistic STARTTLS.
|
|
1353
|
-
//
|
|
1427
|
+
// Opportunistic STARTTLS not offered. The capability list arrived in cleartext, so this
|
|
1428
|
+
// may be a downgrade attack: say so rather than fall back silently (see the doSTARTTLS
|
|
1429
|
+
// option for the policy).
|
|
1430
|
+
this.log.warn({
|
|
1431
|
+
msg: 'Server does not support STARTTLS, continuing over an unencrypted connection',
|
|
1432
|
+
host: this.host,
|
|
1433
|
+
port: this.port,
|
|
1434
|
+
cid: this.id
|
|
1435
|
+
});
|
|
1354
1436
|
return false;
|
|
1355
1437
|
}
|
|
1356
1438
|
/**
|
|
@@ -1613,7 +1695,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1613
1695
|
throw new errors_js_1.AuthenticationFailure('No matching authentication method');
|
|
1614
1696
|
}
|
|
1615
1697
|
/** @internal */
|
|
1616
|
-
beginSession(
|
|
1698
|
+
beginSession() {
|
|
1617
1699
|
(0, tools_js_1.clearTimer)(this.greetingTimeout);
|
|
1618
1700
|
this.greetingReceived = true;
|
|
1619
1701
|
this.untaggedHandlers.OK = null;
|
|
@@ -1632,15 +1714,21 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1632
1714
|
}
|
|
1633
1715
|
})
|
|
1634
1716
|
.catch(err => {
|
|
1635
|
-
|
|
1717
|
+
// The transport goes with the failed attempt: the instance can not be reused, and
|
|
1718
|
+
// an open socket would only trip the inactivity watchdog later
|
|
1719
|
+
this.closeAfter();
|
|
1636
1720
|
if (typeof this.initialReject === 'function') {
|
|
1721
|
+
this.log.error({ err, cid: this.id });
|
|
1637
1722
|
(0, tools_js_1.clearTimer)(this.greetingTimeout);
|
|
1638
1723
|
let reject = this.initialReject;
|
|
1639
1724
|
this.initialResolve = false;
|
|
1640
1725
|
this.initialReject = false;
|
|
1641
1726
|
return reject(err);
|
|
1642
1727
|
}
|
|
1643
|
-
|
|
1728
|
+
// connect() was already settled by whatever took the connection down (a socket
|
|
1729
|
+
// error or close rejected it first), so this failure is a consequence of that.
|
|
1730
|
+
// A second 'error' event for it would throw when no listener is attached.
|
|
1731
|
+
(0, tools_js_1.logConnectionError)(this, 'Session setup failed', err);
|
|
1644
1732
|
});
|
|
1645
1733
|
}
|
|
1646
1734
|
/** @internal */
|
|
@@ -1648,8 +1736,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1648
1736
|
this.greeting = (0, tools_js_1.getTextValues)(message.attributes)
|
|
1649
1737
|
.filter(entry => entry)
|
|
1650
1738
|
.join('');
|
|
1651
|
-
|
|
1652
|
-
this.beginSession(err => this.emitError(err));
|
|
1739
|
+
this.beginSession();
|
|
1653
1740
|
}
|
|
1654
1741
|
/** @internal */
|
|
1655
1742
|
async initialPREAUTH() {
|
|
@@ -1660,10 +1747,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1660
1747
|
// documented contract for the `authenticated` property: `true` when the
|
|
1661
1748
|
// connection was authenticated by a PREAUTH greeting (no credentials known)
|
|
1662
1749
|
this.authenticated = true;
|
|
1663
|
-
this.beginSession(
|
|
1664
|
-
this.log.error({ err, cid: this.id });
|
|
1665
|
-
this.closeAfter();
|
|
1666
|
-
});
|
|
1750
|
+
this.beginSession();
|
|
1667
1751
|
}
|
|
1668
1752
|
/** @internal */
|
|
1669
1753
|
async serverBye(parsed) {
|
|
@@ -1675,10 +1759,13 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1675
1759
|
this.byeReason = reason || 'Server closed connection';
|
|
1676
1760
|
this.untaggedHandlers.BYE = null;
|
|
1677
1761
|
this.state = this.states.LOGOUT;
|
|
1678
|
-
//
|
|
1679
|
-
//
|
|
1680
|
-
// timeout
|
|
1681
|
-
|
|
1762
|
+
// BYE means the server is about to close the connection (RFC 9051 7.1.5), so the socket
|
|
1763
|
+
// is not waited out: a BYE greeting kept open would leave connect() pending until the
|
|
1764
|
+
// greeting timeout, and an unsolicited BYE whose FIN never arrives would leave the
|
|
1765
|
+
// in-flight command and the queue behind it waiting for the socket watchdog. The BYE
|
|
1766
|
+
// that answers a LOGOUT is the exception: its tagged completion closes the connection.
|
|
1767
|
+
const answersLogout = !!this.currentRequest && this.currentRequest.command.toUpperCase() === 'LOGOUT';
|
|
1768
|
+
if (!this.greetingReceived || !answersLogout) {
|
|
1682
1769
|
this.closeAfter();
|
|
1683
1770
|
}
|
|
1684
1771
|
}
|
|
@@ -1740,7 +1827,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1740
1827
|
// keep exists up to date
|
|
1741
1828
|
let prevCount = this.mailbox.exists;
|
|
1742
1829
|
this.mailbox.exists = count;
|
|
1743
|
-
|
|
1830
|
+
(0, tools_js_1.emitSafe)(this, 'exists', {
|
|
1744
1831
|
path: this.mailbox.path,
|
|
1745
1832
|
count,
|
|
1746
1833
|
prevCount
|
|
@@ -1751,7 +1838,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1751
1838
|
/** @internal */
|
|
1752
1839
|
async notifyExpunge(payload) {
|
|
1753
1840
|
if (typeof this.options.expungeHandler !== 'function') {
|
|
1754
|
-
|
|
1841
|
+
(0, tools_js_1.emitSafe)(this, 'expunge', payload);
|
|
1755
1842
|
return;
|
|
1756
1843
|
}
|
|
1757
1844
|
try {
|
|
@@ -1838,7 +1925,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1838
1925
|
if (message.flagColor) {
|
|
1839
1926
|
updateEvent.flagColor = message.flagColor;
|
|
1840
1927
|
}
|
|
1841
|
-
|
|
1928
|
+
(0, tools_js_1.emitSafe)(this, 'flags', updateEvent);
|
|
1842
1929
|
}
|
|
1843
1930
|
}
|
|
1844
1931
|
/** @internal */
|
|
@@ -2022,6 +2109,12 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2022
2109
|
reject(err);
|
|
2023
2110
|
}, deadline.remaining());
|
|
2024
2111
|
let onConnect = () => {
|
|
2112
|
+
// close() may have run between the socket assignment and this callback (the
|
|
2113
|
+
// cleartext proxy path defers it): connect() is already rejected and the socket
|
|
2114
|
+
// gone, so arming the greeting timer now would only fire a stray GREETING_TIMEOUT
|
|
2115
|
+
if (this.isClosed || !this.socket) {
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2025
2118
|
try {
|
|
2026
2119
|
(0, tools_js_1.clearTimer)(this.connectTimeout);
|
|
2027
2120
|
// ImapFlow now owns the socket; drop the proxy's early error handler
|
|
@@ -2156,9 +2249,13 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2156
2249
|
this.idling = false;
|
|
2157
2250
|
this.closeConnectSteps();
|
|
2158
2251
|
if (typeof this.preCheck === 'function') {
|
|
2159
|
-
//
|
|
2160
|
-
//
|
|
2161
|
-
|
|
2252
|
+
// Taken off the connection first: breaking IDLE writes DONE, a write on a dead
|
|
2253
|
+
// transport calls close(), and close() must not find this function here and
|
|
2254
|
+
// re-enter it. The rejection this sees is almost always the NoConnection close()
|
|
2255
|
+
// is about to raise itself.
|
|
2256
|
+
let preCheck = this.preCheck;
|
|
2257
|
+
this.preCheck = false;
|
|
2258
|
+
preCheck().catch(err => (0, tools_js_1.logConnectionError)(this, 'Failed to break IDLE while closing', err));
|
|
2162
2259
|
}
|
|
2163
2260
|
// Session-only public state must not survive the connection it describes: callers read
|
|
2164
2261
|
// these properties in reconnect logic and would otherwise mistake cached objects for
|
|
@@ -2278,6 +2375,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2278
2375
|
closeRequests() {
|
|
2279
2376
|
// Collect all pending requests to reject
|
|
2280
2377
|
let pendingRequests = [];
|
|
2378
|
+
this.commandParts = [];
|
|
2281
2379
|
// reject command that is currently processed
|
|
2282
2380
|
if (this.currentRequest && this.requestTagMap.has(this.currentRequest.tag)) {
|
|
2283
2381
|
let tag = this.currentRequest.tag;
|
|
@@ -3136,7 +3234,34 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3136
3234
|
throw this.createNoConnectionError(false, { rejectedFrom: 'noSocket', command });
|
|
3137
3235
|
}
|
|
3138
3236
|
let handler = this.commands.get(command);
|
|
3139
|
-
|
|
3237
|
+
try {
|
|
3238
|
+
return await handler(this, ...args);
|
|
3239
|
+
}
|
|
3240
|
+
finally {
|
|
3241
|
+
this.releaseOrphanedResponse(command);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
/**
|
|
3245
|
+
* Safety net for the `next()` contract of settleRequest(): a command handler that throws or
|
|
3246
|
+
* returns between the resolution of its exec() and the `next()` call would leave the reader
|
|
3247
|
+
* loop parked, stalling every later command until the socket timeout. Checked one macrotask
|
|
3248
|
+
* after the handler settled, so a concurrently running handler whose response has just been
|
|
3249
|
+
* resolved gets to release it itself first.
|
|
3250
|
+
*
|
|
3251
|
+
* @param command Command name, for the log entry.
|
|
3252
|
+
* @internal
|
|
3253
|
+
*/
|
|
3254
|
+
releaseOrphanedResponse(command) {
|
|
3255
|
+
let release = this.parkedRelease;
|
|
3256
|
+
if (!release) {
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
setImmediate(() => {
|
|
3260
|
+
if (this.parkedRelease === release) {
|
|
3261
|
+
this.log.warn({ msg: 'Command handler did not release its response', command, cid: this.id });
|
|
3262
|
+
release();
|
|
3263
|
+
}
|
|
3264
|
+
});
|
|
3140
3265
|
}
|
|
3141
3266
|
// Mailbox lock queue processor. Implements a mutex pattern: only one lock
|
|
3142
3267
|
// is active at a time. When the active lock is released, the next queued
|
|
@@ -3272,6 +3397,11 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3272
3397
|
try {
|
|
3273
3398
|
// Need to SELECT/EXAMINE a different mailbox
|
|
3274
3399
|
await this.mailboxOpen(path, options);
|
|
3400
|
+
if (!this.mailbox) {
|
|
3401
|
+
// mailboxOpen() resolves with nothing when the connection is no longer
|
|
3402
|
+
// authenticated (a BYE arrived meanwhile): no mailbox, so no lock on it
|
|
3403
|
+
throw this.createNoConnectionError(false, { rejectedFrom: 'mailboxLock', path });
|
|
3404
|
+
}
|
|
3275
3405
|
this.log.trace({
|
|
3276
3406
|
msg: 'Mailbox lock acquired [selected]',
|
|
3277
3407
|
path,
|
|
@@ -3382,35 +3512,36 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3382
3512
|
// {logger:false} never consults mainLogger, so it does not create the default logger
|
|
3383
3513
|
mainLogger = (0, logger_js_1.createConnectionLogger)({ cid: this.id, logRaw: this.options.logRaw });
|
|
3384
3514
|
}
|
|
3515
|
+
// Whether the configured logger takes entries at this level: a disabled logger takes
|
|
3516
|
+
// none, a plain object of level methods the levels it has methods for, plus error and
|
|
3517
|
+
// fatal, which fall back to the console. The level methods and isLevelEnabled() below
|
|
3518
|
+
// share this one rule.
|
|
3519
|
+
const reachesLogger = (level) => this.options.logger !== false && (typeof mainLogger[level] === 'function' || level === 'error' || level === 'fatal');
|
|
3385
3520
|
let synteticLogger = {};
|
|
3386
3521
|
let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|
3387
3522
|
for (let level of levels) {
|
|
3388
3523
|
synteticLogger[level] = (...args) => {
|
|
3389
|
-
|
|
3390
|
-
if (this.options.logger !== false) {
|
|
3524
|
+
if (reachesLogger(level)) {
|
|
3391
3525
|
const logMethod = mainLogger[level];
|
|
3392
|
-
if (typeof logMethod
|
|
3393
|
-
|
|
3394
|
-
// if it isn't supported but the level is error or fatal, log to console anyway.
|
|
3395
|
-
if (level === 'fatal' || level === 'error') {
|
|
3396
|
-
let entry = args[0];
|
|
3397
|
-
try {
|
|
3398
|
-
if (entry && typeof entry === 'object' && entry.err) {
|
|
3399
|
-
entry = Object.assign({}, entry, { err: flattenLoggedError(entry.err) });
|
|
3400
|
-
}
|
|
3401
|
-
console.error(JSON.stringify(entry));
|
|
3402
|
-
}
|
|
3403
|
-
catch {
|
|
3404
|
-
// Serializing failed (a circular structure, a BigInt, a throwing
|
|
3405
|
-
// getter). This fallback exists so an error is never lost, so hand
|
|
3406
|
-
// the entry to console.error itself - it inspects rather than
|
|
3407
|
-
// serializes, and handles all three - instead of dropping it.
|
|
3408
|
-
console.error(entry);
|
|
3409
|
-
}
|
|
3410
|
-
}
|
|
3526
|
+
if (typeof logMethod === 'function') {
|
|
3527
|
+
logMethod.apply(mainLogger, args);
|
|
3411
3528
|
}
|
|
3412
3529
|
else {
|
|
3413
|
-
|
|
3530
|
+
// error or fatal without a method of its own: the console, so it is never lost
|
|
3531
|
+
let entry = args[0];
|
|
3532
|
+
try {
|
|
3533
|
+
if (entry && typeof entry === 'object' && entry.err) {
|
|
3534
|
+
entry = Object.assign({}, entry, { err: flattenLoggedError(entry.err) });
|
|
3535
|
+
}
|
|
3536
|
+
console.error(JSON.stringify(entry));
|
|
3537
|
+
}
|
|
3538
|
+
catch {
|
|
3539
|
+
// Serializing failed (a circular structure, a BigInt, a throwing
|
|
3540
|
+
// getter). This fallback exists so an error is never lost, so hand
|
|
3541
|
+
// the entry to console.error itself - it inspects rather than
|
|
3542
|
+
// serializes, and handles all three - instead of dropping it.
|
|
3543
|
+
console.error(entry);
|
|
3544
|
+
}
|
|
3414
3545
|
}
|
|
3415
3546
|
}
|
|
3416
3547
|
if (this.emitLogs && args && args[0] && typeof args[0] === 'object') {
|
|
@@ -3433,8 +3564,23 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3433
3564
|
}
|
|
3434
3565
|
};
|
|
3435
3566
|
}
|
|
3567
|
+
// Whether an entry at this level reaches anyone, so the caller can skip building an
|
|
3568
|
+
// expensive one (a response serialized for the log) that nobody would see. 'log' events
|
|
3569
|
+
// carry every entry whatever the logger does with it, and a pino-like logger knows its
|
|
3570
|
+
// own threshold.
|
|
3571
|
+
synteticLogger.isLevelEnabled = (level) => this.emitLogs || (typeof mainLogger.isLevelEnabled === 'function' ? !!mainLogger.isLevelEnabled(level) : reachesLogger(level));
|
|
3436
3572
|
return synteticLogger;
|
|
3437
3573
|
}
|
|
3574
|
+
/**
|
|
3575
|
+
* Whether a log entry at this level reaches anyone, so the caller can skip building an
|
|
3576
|
+
* expensive one. A logger assigned to `log` from outside may lack the method, and then
|
|
3577
|
+
* every level counts as enabled.
|
|
3578
|
+
*
|
|
3579
|
+
* @internal
|
|
3580
|
+
*/
|
|
3581
|
+
isLogLevelEnabled(level) {
|
|
3582
|
+
return typeof this.log.isLevelEnabled === 'function' ? this.log.isLevelEnabled(level) : true;
|
|
3583
|
+
}
|
|
3438
3584
|
/**
|
|
3439
3585
|
* Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
|
|
3440
3586
|
* (e.g., STARTTLS) or transferring socket ownership. Call it while the connection is not
|
package/dist/cjs/package-info.js
CHANGED
|
@@ -241,11 +241,11 @@ const httpConnect = async ({ logger, proxyUrl, secureProxy, proxyHost, proxyPort
|
|
|
241
241
|
* @returns An IPv4 address.
|
|
242
242
|
*/
|
|
243
243
|
const resolveIPv4 = async (hostname, deadline) => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
return
|
|
244
|
+
// lookup() goes through the system resolver (getaddrinfo), so hosts files and mDNS names
|
|
245
|
+
// resolve the same way they would for a direct connection. It rejects (ENOTFOUND) rather
|
|
246
|
+
// than returning nothing when the name has no IPv4 address
|
|
247
|
+
let { address } = await deadline.race(node_dns_1.default.promises.lookup(hostname, { family: 4 }));
|
|
248
|
+
return address;
|
|
249
249
|
};
|
|
250
250
|
/**
|
|
251
251
|
* Establishes a tunnel through a SOCKS proxy.
|
|
@@ -289,8 +289,8 @@ const socksConnect = async ({ logger, proxyUrl, protocol, proxyHost, proxyPort,
|
|
|
289
289
|
set_tcp_nodelay: true
|
|
290
290
|
};
|
|
291
291
|
if (proxyUrl.username || proxyUrl.password) {
|
|
292
|
-
connectionOpts.proxy.userId = proxyUrl.username;
|
|
293
|
-
connectionOpts.proxy.password = proxyUrl.password;
|
|
292
|
+
connectionOpts.proxy.userId = decodeUserInfo(proxyUrl.username);
|
|
293
|
+
connectionOpts.proxy.password = decodeUserInfo(proxyUrl.password);
|
|
294
294
|
}
|
|
295
295
|
// The dependency treats a zero timeout as its own 30 second default, so only a strictly
|
|
296
296
|
// positive remaining budget may be passed.
|