imapflow 2.2.6 → 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 +9 -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/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 +19 -10
- 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 +36 -28
- 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 +30 -13
- 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/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 +19 -10
- 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 +37 -29
- 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 +30 -13
- 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 +1 -1
package/dist/esm/imap-flow.js
CHANGED
|
@@ -219,6 +219,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
219
219
|
this.requestTagMap = new Map();
|
|
220
220
|
this.requestQueue = [];
|
|
221
221
|
this.currentRequest = false;
|
|
222
|
+
this.parkedRelease = null;
|
|
222
223
|
this._unknownTagCount = 0;
|
|
223
224
|
this._nextUnknownTagWarn = 1;
|
|
224
225
|
this.writeBytesCounter = 0;
|
|
@@ -248,6 +249,8 @@ export class ImapFlow extends EventEmitter {
|
|
|
248
249
|
this.autoIdleDelay = normalizeAutoIdleDelay(this.options.autoIdleDelay, this.socketTimeout, this.log, this.id);
|
|
249
250
|
this._lastPollAt = 0;
|
|
250
251
|
this._openDownloads = 0;
|
|
252
|
+
this._recoveryPending = false;
|
|
253
|
+
this._processingResponse = false;
|
|
251
254
|
this.missingIdleCommand = (this.options.missingIdleCommand || '').toString().toUpperCase().trim() || 'NOOP';
|
|
252
255
|
this.disableBinary = !!this.options.disableBinary;
|
|
253
256
|
this.idHashAlgorithm = this.options.idHashAlgorithm || 'md5';
|
|
@@ -255,6 +258,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
255
258
|
this.skipListStatusArgs = false;
|
|
256
259
|
this.skipListAuxArgs = false;
|
|
257
260
|
this.skipLsub = false;
|
|
261
|
+
this.skipIdle = false;
|
|
258
262
|
this.skipRev2 = !!this.options.disableIMAP4rev2;
|
|
259
263
|
// Named error handler for proper cleanup. Certain error codes represent
|
|
260
264
|
// expected socket/network issues (buffer exhaustion, connection reset, broken pipe,
|
|
@@ -403,15 +407,11 @@ export class ImapFlow extends EventEmitter {
|
|
|
403
407
|
/** @internal */
|
|
404
408
|
async send(data) {
|
|
405
409
|
if (this.state === this.states.LOGOUT) {
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
request.reject(this.createNoConnectionError(false, { rejectedFrom: 'sendAfterLogout', command: request.command }));
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return;
|
|
410
|
+
// Already logged out. Thrown rather than rejected here so trySend() clears the
|
|
411
|
+
// request from currentRequest and goes on to the next one: a request left there
|
|
412
|
+
// never gets a tagged response, and everything queued behind it would wait for
|
|
413
|
+
// the socket watchdog.
|
|
414
|
+
throw this.createNoConnectionError(false, { rejectedFrom: 'sendAfterLogout', command: data.command });
|
|
415
415
|
}
|
|
416
416
|
// Classify before the first await. Every frame of this command - the command line and
|
|
417
417
|
// any continuation write that follows it - belongs to it until the next send(), because
|
|
@@ -434,14 +434,16 @@ export class ImapFlow extends EventEmitter {
|
|
|
434
434
|
literalMinus: hasCapability(this, 'LITERAL-') || this.capabilities.has('LITERAL+')
|
|
435
435
|
});
|
|
436
436
|
this.commandParts = compiled;
|
|
437
|
-
// Compile again for logging with isLogging=true: masks sensitive values
|
|
438
|
-
// like passwords while producing a human-readable command string
|
|
439
|
-
let logCompiled = await compiler(data, {
|
|
440
|
-
isLogging: true
|
|
441
|
-
});
|
|
442
437
|
/* c8 ignore next */ // send() is always invoked with a request object carrying options, so the {} fallback is unreachable
|
|
443
438
|
let options = data.options || {};
|
|
444
|
-
|
|
439
|
+
// Compile again for logging with isLogging=true: masks sensitive values
|
|
440
|
+
// like passwords while producing a human-readable command string
|
|
441
|
+
if (this.isLogLevelEnabled('debug')) {
|
|
442
|
+
let logCompiled = await compiler(data, {
|
|
443
|
+
isLogging: true
|
|
444
|
+
});
|
|
445
|
+
this.log.debug({ src: 'c', msg: logCompiled.toString(), cid: this.id, comment: options.comment });
|
|
446
|
+
}
|
|
445
447
|
// Send the first part (command text). If there are literal parts,
|
|
446
448
|
// the server will respond with "+" continuations and reader() will
|
|
447
449
|
// send each remaining part from this.commandParts.
|
|
@@ -484,7 +486,6 @@ export class ImapFlow extends EventEmitter {
|
|
|
484
486
|
// nothing reached the wire, so no tagged response ever clears it, and
|
|
485
487
|
// every later command would queue behind it until the socket timeout.
|
|
486
488
|
// Reject the failed command and keep draining the queue.
|
|
487
|
-
this.commandParts = [];
|
|
488
489
|
this.rejectCurrentRequest(err);
|
|
489
490
|
}
|
|
490
491
|
}
|
|
@@ -603,16 +604,53 @@ export class ImapFlow extends EventEmitter {
|
|
|
603
604
|
// a way that leaves the command's outcome unknown.
|
|
604
605
|
/** @internal */
|
|
605
606
|
rejectCurrentRequest(err) {
|
|
607
|
+
let request = this.takeCurrentRequest();
|
|
608
|
+
if (request) {
|
|
609
|
+
request.reject(err);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Takes the in-flight command off the connection: its tag map entry, and the literal parts it
|
|
614
|
+
* still had to send (a tagged NO before the "+" of a refused APPEND leaves them behind, and
|
|
615
|
+
* they must neither stay referenced nor answer a later continuation). The parts are dropped
|
|
616
|
+
* whether or not a command is still current, as a failed dispatch may have cleared it already.
|
|
617
|
+
*
|
|
618
|
+
* @returns The pending request entry, if the command was still current
|
|
619
|
+
* @internal
|
|
620
|
+
*/
|
|
621
|
+
takeCurrentRequest() {
|
|
622
|
+
this.commandParts = [];
|
|
606
623
|
if (!this.currentRequest) {
|
|
607
|
-
return;
|
|
624
|
+
return undefined;
|
|
608
625
|
}
|
|
609
626
|
let tag = this.currentRequest.tag;
|
|
610
627
|
this.currentRequest = false;
|
|
611
628
|
let request = this.requestTagMap.get(tag);
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
629
|
+
this.requestTagMap.delete(tag);
|
|
630
|
+
return request;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Resolves a command's promise with its tagged response and parks the reader until the
|
|
634
|
+
* command's handler calls next(). The release is kept in parkedRelease so a handler that
|
|
635
|
+
* throws before calling next() can be unparked (see releaseOrphanedResponse()); every path
|
|
636
|
+
* that resolves a command this way goes through here for that reason.
|
|
637
|
+
*
|
|
638
|
+
* @param request - Pending request entry
|
|
639
|
+
* @param parsed - Parsed tagged response
|
|
640
|
+
* @param hasTrailingData - Whether more input was already buffered after this line
|
|
641
|
+
* @internal
|
|
642
|
+
*/
|
|
643
|
+
resolveParked(request, parsed, hasTrailingData) {
|
|
644
|
+
return new Promise(resolve => {
|
|
645
|
+
let release = () => {
|
|
646
|
+
if (this.parkedRelease === release) {
|
|
647
|
+
this.parkedRelease = null;
|
|
648
|
+
}
|
|
649
|
+
resolve();
|
|
650
|
+
};
|
|
651
|
+
this.parkedRelease = release;
|
|
652
|
+
request.resolve({ response: parsed, next: release, hasTrailingData });
|
|
653
|
+
});
|
|
616
654
|
}
|
|
617
655
|
/**
|
|
618
656
|
* Waits out a throttle back-off.
|
|
@@ -646,6 +684,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
646
684
|
while ((data = this.streamer.read()) !== null) {
|
|
647
685
|
let keepReading;
|
|
648
686
|
try {
|
|
687
|
+
// The reader is parked here for as long as processing takes (a fetch consumer
|
|
688
|
+
// working on a row), so a socket that goes quiet meanwhile is this side's doing,
|
|
689
|
+
// see _socketTimeout
|
|
690
|
+
this._processingResponse = true;
|
|
649
691
|
keepReading = await this.handleResponse(data);
|
|
650
692
|
}
|
|
651
693
|
catch (err) {
|
|
@@ -662,6 +704,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
662
704
|
this.failProtocol(error);
|
|
663
705
|
}
|
|
664
706
|
finally {
|
|
707
|
+
this._processingResponse = false;
|
|
665
708
|
this.releaseStreamData(data);
|
|
666
709
|
}
|
|
667
710
|
if (!keepReading) {
|
|
@@ -753,15 +796,14 @@ export class ImapFlow extends EventEmitter {
|
|
|
753
796
|
this.log.warn({ err, cid: this.id });
|
|
754
797
|
}
|
|
755
798
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if (
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
this.log.debug({ src: 's', msg: logCompiled.toString(), cid: this.id, nullBytesRemoved: parsed.nullBytesRemoved });
|
|
799
|
+
// FETCH responses are the bulk of a session, so they log at trace. The serialization is
|
|
800
|
+
// skipped when the level is off.
|
|
801
|
+
let logLevel = /^\d+$/.test(parsed.command || '') && parsed.attributes && parsed.attributes[0] && parsed.attributes[0].value === 'FETCH' ? 'trace' : 'debug';
|
|
802
|
+
if (this.isLogLevelEnabled(logLevel)) {
|
|
803
|
+
let logCompiled = await compiler(parsed, {
|
|
804
|
+
isLogging: true
|
|
805
|
+
});
|
|
806
|
+
this.log[logLevel]({ src: 's', msg: logCompiled.toString(), cid: this.id, nullBytesRemoved: parsed.nullBytesRemoved });
|
|
765
807
|
}
|
|
766
808
|
// IMAP "+" (continuation request) handling. The server sends "+" in two cases:
|
|
767
809
|
// 1. During IDLE or AUTHENTICATE, where a custom handler (onPlusTag) processes it
|
|
@@ -837,9 +879,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
837
879
|
// and an entirely unknown tag is recorded but tolerated.
|
|
838
880
|
if (parsed.tag && !['*', '+'].includes(parsed.tag)) {
|
|
839
881
|
if (this.currentRequest && this.currentRequest.tag === parsed.tag && this.currentRequest.sent) {
|
|
840
|
-
let request = this.
|
|
841
|
-
this.requestTagMap.delete(parsed.tag);
|
|
842
|
-
this.currentRequest = false;
|
|
882
|
+
let request = this.takeCurrentRequest();
|
|
843
883
|
if (request) {
|
|
844
884
|
await this.settleRequest(request, parsed, !!data.trailingAfterLine);
|
|
845
885
|
}
|
|
@@ -899,7 +939,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
899
939
|
case 'BYE':
|
|
900
940
|
// hasTrailingData is forwarded so STARTTLS can detect a plaintext
|
|
901
941
|
// injection (data buffered after the tagged OK, before the handshake).
|
|
902
|
-
await
|
|
942
|
+
await this.resolveParked(request, parsed, hasTrailingData);
|
|
903
943
|
break;
|
|
904
944
|
case 'NO':
|
|
905
945
|
case 'BAD': {
|
|
@@ -927,7 +967,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
927
967
|
// is told nothing else about it, so this entry is the only record that
|
|
928
968
|
// the response was truncated.
|
|
929
969
|
this.log.warn({ msg: 'Partial FETCH response', cid: this.id, err });
|
|
930
|
-
await
|
|
970
|
+
await this.resolveParked(request, parsed);
|
|
931
971
|
break;
|
|
932
972
|
}
|
|
933
973
|
let throttleDelay = false;
|
|
@@ -1021,6 +1061,16 @@ export class ImapFlow extends EventEmitter {
|
|
|
1021
1061
|
if (typeof socket.setKeepAlive === 'function') {
|
|
1022
1062
|
socket.setKeepAlive(true, 5 * 1000);
|
|
1023
1063
|
}
|
|
1064
|
+
this.armSocketTimeout(socket);
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Arms the inactivity watchdog on a socket. Guarded the same way as the rest of
|
|
1068
|
+
* configureSocket(): other runtimes stub socket features.
|
|
1069
|
+
*
|
|
1070
|
+
* @param socket - The socket to arm
|
|
1071
|
+
* @internal
|
|
1072
|
+
*/
|
|
1073
|
+
armSocketTimeout(socket) {
|
|
1024
1074
|
if (typeof socket.setTimeout === 'function') {
|
|
1025
1075
|
socket.setTimeout(this.socketTimeout);
|
|
1026
1076
|
}
|
|
@@ -1060,25 +1110,50 @@ export class ImapFlow extends EventEmitter {
|
|
|
1060
1110
|
this._socketTimeout =
|
|
1061
1111
|
this._socketTimeout ||
|
|
1062
1112
|
(() => {
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1113
|
+
const timeoutError = () => {
|
|
1114
|
+
const err = new Error('Socket timeout');
|
|
1115
|
+
err.code = 'ETIMEOUT';
|
|
1116
|
+
return err;
|
|
1117
|
+
};
|
|
1118
|
+
if (!this.usable || !this.socket || this.socket.destroyed) {
|
|
1119
|
+
this.emitError(timeoutError());
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (this._processingResponse) {
|
|
1123
|
+
// The reader is parked processing a response (a fetch consumer taking its
|
|
1124
|
+
// time over a row), so nothing is being read: the quiet is this side's, and
|
|
1125
|
+
// the server is not overdue with anything. The timer is one-shot and nothing
|
|
1126
|
+
// resumes it until reading does, so it is re-armed for the next check.
|
|
1127
|
+
this.log.debug({ msg: 'Socket timeout while a response is being processed', cid: this.id });
|
|
1128
|
+
this.armSocketTimeout(this.socket);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
// A throttle back-off is quiet by design, the NOOP below probes that the server
|
|
1132
|
+
// is still there
|
|
1133
|
+
const quietExpected = this.idling || this._openDownloads || this.currentLock || this._throttleWaits.size;
|
|
1066
1134
|
const commandStuck = this.currentRequest && !(this.idling && this.currentRequest.command === 'IDLE');
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1135
|
+
// A recovery NOOP that has not settled by the next timeout means the peer is gone
|
|
1136
|
+
if (quietExpected && !commandStuck && !this._recoveryPending) {
|
|
1137
|
+
// The inactivity timer is one-shot, re-armed only by traffic. The NOOP can not
|
|
1138
|
+
// reach the wire while IDLE still waits for its continuation (DONE is sent
|
|
1139
|
+
// only after the "+"), so without re-arming it here a peer that went silent
|
|
1140
|
+
// right after IDLE would never time out again.
|
|
1141
|
+
this._recoveryPending = true;
|
|
1142
|
+
this.armSocketTimeout(this.socket);
|
|
1143
|
+
this.run('NOOP')
|
|
1144
|
+
.catch(err => {
|
|
1073
1145
|
this.log.warn({ msg: 'Connection recovery failed after timeout', err, cid: this.id });
|
|
1074
1146
|
if (!this.isClosed) {
|
|
1075
1147
|
this.close();
|
|
1076
1148
|
}
|
|
1149
|
+
})
|
|
1150
|
+
.finally(() => {
|
|
1151
|
+
this._recoveryPending = false;
|
|
1077
1152
|
});
|
|
1078
1153
|
}
|
|
1079
1154
|
else {
|
|
1080
1155
|
this.log.debug({ msg: 'Socket timeout', cid: this.id });
|
|
1081
|
-
this.emitError(
|
|
1156
|
+
this.emitError(timeoutError());
|
|
1082
1157
|
}
|
|
1083
1158
|
});
|
|
1084
1159
|
const socket = this.socket;
|
|
@@ -1308,8 +1383,15 @@ export class ImapFlow extends EventEmitter {
|
|
|
1308
1383
|
err.tlsFailed = true;
|
|
1309
1384
|
throw err;
|
|
1310
1385
|
}
|
|
1311
|
-
// Opportunistic STARTTLS.
|
|
1312
|
-
//
|
|
1386
|
+
// Opportunistic STARTTLS not offered. The capability list arrived in cleartext, so this
|
|
1387
|
+
// may be a downgrade attack: say so rather than fall back silently (see the doSTARTTLS
|
|
1388
|
+
// option for the policy).
|
|
1389
|
+
this.log.warn({
|
|
1390
|
+
msg: 'Server does not support STARTTLS, continuing over an unencrypted connection',
|
|
1391
|
+
host: this.host,
|
|
1392
|
+
port: this.port,
|
|
1393
|
+
cid: this.id
|
|
1394
|
+
});
|
|
1313
1395
|
return false;
|
|
1314
1396
|
}
|
|
1315
1397
|
/**
|
|
@@ -1572,7 +1654,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1572
1654
|
throw new AuthenticationFailure('No matching authentication method');
|
|
1573
1655
|
}
|
|
1574
1656
|
/** @internal */
|
|
1575
|
-
beginSession(
|
|
1657
|
+
beginSession() {
|
|
1576
1658
|
clearTimer(this.greetingTimeout);
|
|
1577
1659
|
this.greetingReceived = true;
|
|
1578
1660
|
this.untaggedHandlers.OK = null;
|
|
@@ -1591,15 +1673,21 @@ export class ImapFlow extends EventEmitter {
|
|
|
1591
1673
|
}
|
|
1592
1674
|
})
|
|
1593
1675
|
.catch(err => {
|
|
1594
|
-
|
|
1676
|
+
// The transport goes with the failed attempt: the instance can not be reused, and
|
|
1677
|
+
// an open socket would only trip the inactivity watchdog later
|
|
1678
|
+
this.closeAfter();
|
|
1595
1679
|
if (typeof this.initialReject === 'function') {
|
|
1680
|
+
this.log.error({ err, cid: this.id });
|
|
1596
1681
|
clearTimer(this.greetingTimeout);
|
|
1597
1682
|
let reject = this.initialReject;
|
|
1598
1683
|
this.initialResolve = false;
|
|
1599
1684
|
this.initialReject = false;
|
|
1600
1685
|
return reject(err);
|
|
1601
1686
|
}
|
|
1602
|
-
|
|
1687
|
+
// connect() was already settled by whatever took the connection down (a socket
|
|
1688
|
+
// error or close rejected it first), so this failure is a consequence of that.
|
|
1689
|
+
// A second 'error' event for it would throw when no listener is attached.
|
|
1690
|
+
logConnectionError(this, 'Session setup failed', err);
|
|
1603
1691
|
});
|
|
1604
1692
|
}
|
|
1605
1693
|
/** @internal */
|
|
@@ -1607,8 +1695,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1607
1695
|
this.greeting = getTextValues(message.attributes)
|
|
1608
1696
|
.filter(entry => entry)
|
|
1609
1697
|
.join('');
|
|
1610
|
-
|
|
1611
|
-
this.beginSession(err => this.emitError(err));
|
|
1698
|
+
this.beginSession();
|
|
1612
1699
|
}
|
|
1613
1700
|
/** @internal */
|
|
1614
1701
|
async initialPREAUTH() {
|
|
@@ -1619,10 +1706,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1619
1706
|
// documented contract for the `authenticated` property: `true` when the
|
|
1620
1707
|
// connection was authenticated by a PREAUTH greeting (no credentials known)
|
|
1621
1708
|
this.authenticated = true;
|
|
1622
|
-
this.beginSession(
|
|
1623
|
-
this.log.error({ err, cid: this.id });
|
|
1624
|
-
this.closeAfter();
|
|
1625
|
-
});
|
|
1709
|
+
this.beginSession();
|
|
1626
1710
|
}
|
|
1627
1711
|
/** @internal */
|
|
1628
1712
|
async serverBye(parsed) {
|
|
@@ -1634,10 +1718,13 @@ export class ImapFlow extends EventEmitter {
|
|
|
1634
1718
|
this.byeReason = reason || 'Server closed connection';
|
|
1635
1719
|
this.untaggedHandlers.BYE = null;
|
|
1636
1720
|
this.state = this.states.LOGOUT;
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
1639
|
-
// timeout
|
|
1640
|
-
|
|
1721
|
+
// BYE means the server is about to close the connection (RFC 9051 7.1.5), so the socket
|
|
1722
|
+
// is not waited out: a BYE greeting kept open would leave connect() pending until the
|
|
1723
|
+
// greeting timeout, and an unsolicited BYE whose FIN never arrives would leave the
|
|
1724
|
+
// in-flight command and the queue behind it waiting for the socket watchdog. The BYE
|
|
1725
|
+
// that answers a LOGOUT is the exception: its tagged completion closes the connection.
|
|
1726
|
+
const answersLogout = !!this.currentRequest && this.currentRequest.command.toUpperCase() === 'LOGOUT';
|
|
1727
|
+
if (!this.greetingReceived || !answersLogout) {
|
|
1641
1728
|
this.closeAfter();
|
|
1642
1729
|
}
|
|
1643
1730
|
}
|
|
@@ -1699,7 +1786,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1699
1786
|
// keep exists up to date
|
|
1700
1787
|
let prevCount = this.mailbox.exists;
|
|
1701
1788
|
this.mailbox.exists = count;
|
|
1702
|
-
this
|
|
1789
|
+
emitSafe(this, 'exists', {
|
|
1703
1790
|
path: this.mailbox.path,
|
|
1704
1791
|
count,
|
|
1705
1792
|
prevCount
|
|
@@ -1710,7 +1797,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1710
1797
|
/** @internal */
|
|
1711
1798
|
async notifyExpunge(payload) {
|
|
1712
1799
|
if (typeof this.options.expungeHandler !== 'function') {
|
|
1713
|
-
this
|
|
1800
|
+
emitSafe(this, 'expunge', payload);
|
|
1714
1801
|
return;
|
|
1715
1802
|
}
|
|
1716
1803
|
try {
|
|
@@ -1797,7 +1884,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1797
1884
|
if (message.flagColor) {
|
|
1798
1885
|
updateEvent.flagColor = message.flagColor;
|
|
1799
1886
|
}
|
|
1800
|
-
this
|
|
1887
|
+
emitSafe(this, 'flags', updateEvent);
|
|
1801
1888
|
}
|
|
1802
1889
|
}
|
|
1803
1890
|
/** @internal */
|
|
@@ -1981,6 +2068,12 @@ export class ImapFlow extends EventEmitter {
|
|
|
1981
2068
|
reject(err);
|
|
1982
2069
|
}, deadline.remaining());
|
|
1983
2070
|
let onConnect = () => {
|
|
2071
|
+
// close() may have run between the socket assignment and this callback (the
|
|
2072
|
+
// cleartext proxy path defers it): connect() is already rejected and the socket
|
|
2073
|
+
// gone, so arming the greeting timer now would only fire a stray GREETING_TIMEOUT
|
|
2074
|
+
if (this.isClosed || !this.socket) {
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
1984
2077
|
try {
|
|
1985
2078
|
clearTimer(this.connectTimeout);
|
|
1986
2079
|
// ImapFlow now owns the socket; drop the proxy's early error handler
|
|
@@ -2115,9 +2208,13 @@ export class ImapFlow extends EventEmitter {
|
|
|
2115
2208
|
this.idling = false;
|
|
2116
2209
|
this.closeConnectSteps();
|
|
2117
2210
|
if (typeof this.preCheck === 'function') {
|
|
2118
|
-
//
|
|
2119
|
-
//
|
|
2120
|
-
|
|
2211
|
+
// Taken off the connection first: breaking IDLE writes DONE, a write on a dead
|
|
2212
|
+
// transport calls close(), and close() must not find this function here and
|
|
2213
|
+
// re-enter it. The rejection this sees is almost always the NoConnection close()
|
|
2214
|
+
// is about to raise itself.
|
|
2215
|
+
let preCheck = this.preCheck;
|
|
2216
|
+
this.preCheck = false;
|
|
2217
|
+
preCheck().catch(err => logConnectionError(this, 'Failed to break IDLE while closing', err));
|
|
2121
2218
|
}
|
|
2122
2219
|
// Session-only public state must not survive the connection it describes: callers read
|
|
2123
2220
|
// these properties in reconnect logic and would otherwise mistake cached objects for
|
|
@@ -2237,6 +2334,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
2237
2334
|
closeRequests() {
|
|
2238
2335
|
// Collect all pending requests to reject
|
|
2239
2336
|
let pendingRequests = [];
|
|
2337
|
+
this.commandParts = [];
|
|
2240
2338
|
// reject command that is currently processed
|
|
2241
2339
|
if (this.currentRequest && this.requestTagMap.has(this.currentRequest.tag)) {
|
|
2242
2340
|
let tag = this.currentRequest.tag;
|
|
@@ -3095,7 +3193,34 @@ export class ImapFlow extends EventEmitter {
|
|
|
3095
3193
|
throw this.createNoConnectionError(false, { rejectedFrom: 'noSocket', command });
|
|
3096
3194
|
}
|
|
3097
3195
|
let handler = this.commands.get(command);
|
|
3098
|
-
|
|
3196
|
+
try {
|
|
3197
|
+
return await handler(this, ...args);
|
|
3198
|
+
}
|
|
3199
|
+
finally {
|
|
3200
|
+
this.releaseOrphanedResponse(command);
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
/**
|
|
3204
|
+
* Safety net for the `next()` contract of settleRequest(): a command handler that throws or
|
|
3205
|
+
* returns between the resolution of its exec() and the `next()` call would leave the reader
|
|
3206
|
+
* loop parked, stalling every later command until the socket timeout. Checked one macrotask
|
|
3207
|
+
* after the handler settled, so a concurrently running handler whose response has just been
|
|
3208
|
+
* resolved gets to release it itself first.
|
|
3209
|
+
*
|
|
3210
|
+
* @param command Command name, for the log entry.
|
|
3211
|
+
* @internal
|
|
3212
|
+
*/
|
|
3213
|
+
releaseOrphanedResponse(command) {
|
|
3214
|
+
let release = this.parkedRelease;
|
|
3215
|
+
if (!release) {
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
3218
|
+
setImmediate(() => {
|
|
3219
|
+
if (this.parkedRelease === release) {
|
|
3220
|
+
this.log.warn({ msg: 'Command handler did not release its response', command, cid: this.id });
|
|
3221
|
+
release();
|
|
3222
|
+
}
|
|
3223
|
+
});
|
|
3099
3224
|
}
|
|
3100
3225
|
// Mailbox lock queue processor. Implements a mutex pattern: only one lock
|
|
3101
3226
|
// is active at a time. When the active lock is released, the next queued
|
|
@@ -3231,6 +3356,11 @@ export class ImapFlow extends EventEmitter {
|
|
|
3231
3356
|
try {
|
|
3232
3357
|
// Need to SELECT/EXAMINE a different mailbox
|
|
3233
3358
|
await this.mailboxOpen(path, options);
|
|
3359
|
+
if (!this.mailbox) {
|
|
3360
|
+
// mailboxOpen() resolves with nothing when the connection is no longer
|
|
3361
|
+
// authenticated (a BYE arrived meanwhile): no mailbox, so no lock on it
|
|
3362
|
+
throw this.createNoConnectionError(false, { rejectedFrom: 'mailboxLock', path });
|
|
3363
|
+
}
|
|
3234
3364
|
this.log.trace({
|
|
3235
3365
|
msg: 'Mailbox lock acquired [selected]',
|
|
3236
3366
|
path,
|
|
@@ -3341,35 +3471,36 @@ export class ImapFlow extends EventEmitter {
|
|
|
3341
3471
|
// {logger:false} never consults mainLogger, so it does not create the default logger
|
|
3342
3472
|
mainLogger = createConnectionLogger({ cid: this.id, logRaw: this.options.logRaw });
|
|
3343
3473
|
}
|
|
3474
|
+
// Whether the configured logger takes entries at this level: a disabled logger takes
|
|
3475
|
+
// none, a plain object of level methods the levels it has methods for, plus error and
|
|
3476
|
+
// fatal, which fall back to the console. The level methods and isLevelEnabled() below
|
|
3477
|
+
// share this one rule.
|
|
3478
|
+
const reachesLogger = (level) => this.options.logger !== false && (typeof mainLogger[level] === 'function' || level === 'error' || level === 'fatal');
|
|
3344
3479
|
let synteticLogger = {};
|
|
3345
3480
|
let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|
3346
3481
|
for (let level of levels) {
|
|
3347
3482
|
synteticLogger[level] = (...args) => {
|
|
3348
|
-
|
|
3349
|
-
if (this.options.logger !== false) {
|
|
3483
|
+
if (reachesLogger(level)) {
|
|
3350
3484
|
const logMethod = mainLogger[level];
|
|
3351
|
-
if (typeof logMethod
|
|
3352
|
-
|
|
3353
|
-
// if it isn't supported but the level is error or fatal, log to console anyway.
|
|
3354
|
-
if (level === 'fatal' || level === 'error') {
|
|
3355
|
-
let entry = args[0];
|
|
3356
|
-
try {
|
|
3357
|
-
if (entry && typeof entry === 'object' && entry.err) {
|
|
3358
|
-
entry = Object.assign({}, entry, { err: flattenLoggedError(entry.err) });
|
|
3359
|
-
}
|
|
3360
|
-
console.error(JSON.stringify(entry));
|
|
3361
|
-
}
|
|
3362
|
-
catch {
|
|
3363
|
-
// Serializing failed (a circular structure, a BigInt, a throwing
|
|
3364
|
-
// getter). This fallback exists so an error is never lost, so hand
|
|
3365
|
-
// the entry to console.error itself - it inspects rather than
|
|
3366
|
-
// serializes, and handles all three - instead of dropping it.
|
|
3367
|
-
console.error(entry);
|
|
3368
|
-
}
|
|
3369
|
-
}
|
|
3485
|
+
if (typeof logMethod === 'function') {
|
|
3486
|
+
logMethod.apply(mainLogger, args);
|
|
3370
3487
|
}
|
|
3371
3488
|
else {
|
|
3372
|
-
|
|
3489
|
+
// error or fatal without a method of its own: the console, so it is never lost
|
|
3490
|
+
let entry = args[0];
|
|
3491
|
+
try {
|
|
3492
|
+
if (entry && typeof entry === 'object' && entry.err) {
|
|
3493
|
+
entry = Object.assign({}, entry, { err: flattenLoggedError(entry.err) });
|
|
3494
|
+
}
|
|
3495
|
+
console.error(JSON.stringify(entry));
|
|
3496
|
+
}
|
|
3497
|
+
catch {
|
|
3498
|
+
// Serializing failed (a circular structure, a BigInt, a throwing
|
|
3499
|
+
// getter). This fallback exists so an error is never lost, so hand
|
|
3500
|
+
// the entry to console.error itself - it inspects rather than
|
|
3501
|
+
// serializes, and handles all three - instead of dropping it.
|
|
3502
|
+
console.error(entry);
|
|
3503
|
+
}
|
|
3373
3504
|
}
|
|
3374
3505
|
}
|
|
3375
3506
|
if (this.emitLogs && args && args[0] && typeof args[0] === 'object') {
|
|
@@ -3392,8 +3523,23 @@ export class ImapFlow extends EventEmitter {
|
|
|
3392
3523
|
}
|
|
3393
3524
|
};
|
|
3394
3525
|
}
|
|
3526
|
+
// Whether an entry at this level reaches anyone, so the caller can skip building an
|
|
3527
|
+
// expensive one (a response serialized for the log) that nobody would see. 'log' events
|
|
3528
|
+
// carry every entry whatever the logger does with it, and a pino-like logger knows its
|
|
3529
|
+
// own threshold.
|
|
3530
|
+
synteticLogger.isLevelEnabled = (level) => this.emitLogs || (typeof mainLogger.isLevelEnabled === 'function' ? !!mainLogger.isLevelEnabled(level) : reachesLogger(level));
|
|
3395
3531
|
return synteticLogger;
|
|
3396
3532
|
}
|
|
3533
|
+
/**
|
|
3534
|
+
* Whether a log entry at this level reaches anyone, so the caller can skip building an
|
|
3535
|
+
* expensive one. A logger assigned to `log` from outside may lack the method, and then
|
|
3536
|
+
* every level counts as enabled.
|
|
3537
|
+
*
|
|
3538
|
+
* @internal
|
|
3539
|
+
*/
|
|
3540
|
+
isLogLevelEnabled(level) {
|
|
3541
|
+
return typeof this.log.isLevelEnabled === 'function' ? this.log.isLevelEnabled(level) : true;
|
|
3542
|
+
}
|
|
3397
3543
|
/**
|
|
3398
3544
|
* Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
|
|
3399
3545
|
* (e.g., STARTTLS) or transferring socket ownership. Call it while the connection is not
|
package/dist/esm/package-info.js
CHANGED
|
@@ -234,11 +234,11 @@ const httpConnect = async ({ logger, proxyUrl, secureProxy, proxyHost, proxyPort
|
|
|
234
234
|
* @returns An IPv4 address.
|
|
235
235
|
*/
|
|
236
236
|
const resolveIPv4 = async (hostname, deadline) => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
return
|
|
237
|
+
// lookup() goes through the system resolver (getaddrinfo), so hosts files and mDNS names
|
|
238
|
+
// resolve the same way they would for a direct connection. It rejects (ENOTFOUND) rather
|
|
239
|
+
// than returning nothing when the name has no IPv4 address
|
|
240
|
+
let { address } = await deadline.race(dns.promises.lookup(hostname, { family: 4 }));
|
|
241
|
+
return address;
|
|
242
242
|
};
|
|
243
243
|
/**
|
|
244
244
|
* Establishes a tunnel through a SOCKS proxy.
|
|
@@ -282,8 +282,8 @@ const socksConnect = async ({ logger, proxyUrl, protocol, proxyHost, proxyPort,
|
|
|
282
282
|
set_tcp_nodelay: true
|
|
283
283
|
};
|
|
284
284
|
if (proxyUrl.username || proxyUrl.password) {
|
|
285
|
-
connectionOpts.proxy.userId = proxyUrl.username;
|
|
286
|
-
connectionOpts.proxy.password = proxyUrl.password;
|
|
285
|
+
connectionOpts.proxy.userId = decodeUserInfo(proxyUrl.username);
|
|
286
|
+
connectionOpts.proxy.password = decodeUserInfo(proxyUrl.password);
|
|
287
287
|
}
|
|
288
288
|
// The dependency treats a zero timeout as its own 30 second default, so only a strictly
|
|
289
289
|
// positive remaining budget may be passed.
|