imapflow 2.2.6 → 2.2.8
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 +24 -0
- package/dist/cjs/commands/append.js +10 -3
- package/dist/cjs/commands/authenticate.js +42 -22
- 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/enable.js +6 -0
- package/dist/cjs/commands/esearch-parser.js +8 -2
- package/dist/cjs/commands/fetch.js +57 -14
- 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 +8 -8
- package/dist/cjs/download.js +220 -95
- 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.d.ts +2 -2
- package/dist/cjs/imap-flow.js +256 -95
- 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 +33 -13
- package/dist/cjs/special-use.js +10 -5
- package/dist/cjs/tools.d.ts +14 -4
- package/dist/cjs/tools.js +69 -9
- package/dist/cjs/types.d.ts +19 -4
- package/dist/esm/commands/append.js +11 -4
- package/dist/esm/commands/authenticate.js +43 -23
- 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/enable.js +6 -0
- package/dist/esm/commands/esearch-parser.js +8 -2
- package/dist/esm/commands/fetch.js +57 -14
- 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 +9 -9
- package/dist/esm/download.js +220 -95
- 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.d.ts +2 -2
- package/dist/esm/imap-flow.js +256 -95
- 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 +33 -13
- package/dist/esm/special-use.js +10 -5
- package/dist/esm/tools.d.ts +14 -4
- package/dist/esm/tools.js +68 -9
- package/dist/esm/types.d.ts +19 -4
- package/package.json +5 -3
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
|
/**
|
|
@@ -1377,9 +1459,8 @@ export class ImapFlow extends EventEmitter {
|
|
|
1377
1459
|
// of the IP - accepting any "localhost" certificate for any IP-hosted
|
|
1378
1460
|
// server, and rejecting legitimate IP-SAN certificates.
|
|
1379
1461
|
host: this.host,
|
|
1380
|
-
servername: this.servername,
|
|
1381
1462
|
port: this.port
|
|
1382
|
-
}, this.options.tls || {});
|
|
1463
|
+
}, this.tlsServername(), this.options.tls || {});
|
|
1383
1464
|
this.clearSocketHandlers();
|
|
1384
1465
|
let settled = false;
|
|
1385
1466
|
// Single settlement path for the upgrade. Every terminal outcome - handshake
|
|
@@ -1572,7 +1653,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1572
1653
|
throw new AuthenticationFailure('No matching authentication method');
|
|
1573
1654
|
}
|
|
1574
1655
|
/** @internal */
|
|
1575
|
-
beginSession(
|
|
1656
|
+
beginSession() {
|
|
1576
1657
|
clearTimer(this.greetingTimeout);
|
|
1577
1658
|
this.greetingReceived = true;
|
|
1578
1659
|
this.untaggedHandlers.OK = null;
|
|
@@ -1591,15 +1672,21 @@ export class ImapFlow extends EventEmitter {
|
|
|
1591
1672
|
}
|
|
1592
1673
|
})
|
|
1593
1674
|
.catch(err => {
|
|
1594
|
-
|
|
1675
|
+
// The transport goes with the failed attempt: the instance can not be reused, and
|
|
1676
|
+
// an open socket would only trip the inactivity watchdog later
|
|
1677
|
+
this.closeAfter();
|
|
1595
1678
|
if (typeof this.initialReject === 'function') {
|
|
1679
|
+
this.log.error({ err, cid: this.id });
|
|
1596
1680
|
clearTimer(this.greetingTimeout);
|
|
1597
1681
|
let reject = this.initialReject;
|
|
1598
1682
|
this.initialResolve = false;
|
|
1599
1683
|
this.initialReject = false;
|
|
1600
1684
|
return reject(err);
|
|
1601
1685
|
}
|
|
1602
|
-
|
|
1686
|
+
// connect() was already settled by whatever took the connection down (a socket
|
|
1687
|
+
// error or close rejected it first), so this failure is a consequence of that.
|
|
1688
|
+
// A second 'error' event for it would throw when no listener is attached.
|
|
1689
|
+
logConnectionError(this, 'Session setup failed', err);
|
|
1603
1690
|
});
|
|
1604
1691
|
}
|
|
1605
1692
|
/** @internal */
|
|
@@ -1607,8 +1694,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1607
1694
|
this.greeting = getTextValues(message.attributes)
|
|
1608
1695
|
.filter(entry => entry)
|
|
1609
1696
|
.join('');
|
|
1610
|
-
|
|
1611
|
-
this.beginSession(err => this.emitError(err));
|
|
1697
|
+
this.beginSession();
|
|
1612
1698
|
}
|
|
1613
1699
|
/** @internal */
|
|
1614
1700
|
async initialPREAUTH() {
|
|
@@ -1619,10 +1705,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1619
1705
|
// documented contract for the `authenticated` property: `true` when the
|
|
1620
1706
|
// connection was authenticated by a PREAUTH greeting (no credentials known)
|
|
1621
1707
|
this.authenticated = true;
|
|
1622
|
-
this.beginSession(
|
|
1623
|
-
this.log.error({ err, cid: this.id });
|
|
1624
|
-
this.closeAfter();
|
|
1625
|
-
});
|
|
1708
|
+
this.beginSession();
|
|
1626
1709
|
}
|
|
1627
1710
|
/** @internal */
|
|
1628
1711
|
async serverBye(parsed) {
|
|
@@ -1634,10 +1717,13 @@ export class ImapFlow extends EventEmitter {
|
|
|
1634
1717
|
this.byeReason = reason || 'Server closed connection';
|
|
1635
1718
|
this.untaggedHandlers.BYE = null;
|
|
1636
1719
|
this.state = this.states.LOGOUT;
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
1639
|
-
// timeout
|
|
1640
|
-
|
|
1720
|
+
// BYE means the server is about to close the connection (RFC 9051 7.1.5), so the socket
|
|
1721
|
+
// is not waited out: a BYE greeting kept open would leave connect() pending until the
|
|
1722
|
+
// greeting timeout, and an unsolicited BYE whose FIN never arrives would leave the
|
|
1723
|
+
// in-flight command and the queue behind it waiting for the socket watchdog. The BYE
|
|
1724
|
+
// that answers a LOGOUT is the exception: its tagged completion closes the connection.
|
|
1725
|
+
const answersLogout = !!this.currentRequest && this.currentRequest.command.toUpperCase() === 'LOGOUT';
|
|
1726
|
+
if (!this.greetingReceived || !answersLogout) {
|
|
1641
1727
|
this.closeAfter();
|
|
1642
1728
|
}
|
|
1643
1729
|
}
|
|
@@ -1699,7 +1785,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1699
1785
|
// keep exists up to date
|
|
1700
1786
|
let prevCount = this.mailbox.exists;
|
|
1701
1787
|
this.mailbox.exists = count;
|
|
1702
|
-
this
|
|
1788
|
+
emitSafe(this, 'exists', {
|
|
1703
1789
|
path: this.mailbox.path,
|
|
1704
1790
|
count,
|
|
1705
1791
|
prevCount
|
|
@@ -1710,7 +1796,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1710
1796
|
/** @internal */
|
|
1711
1797
|
async notifyExpunge(payload) {
|
|
1712
1798
|
if (typeof this.options.expungeHandler !== 'function') {
|
|
1713
|
-
this
|
|
1799
|
+
emitSafe(this, 'expunge', payload);
|
|
1714
1800
|
return;
|
|
1715
1801
|
}
|
|
1716
1802
|
try {
|
|
@@ -1764,12 +1850,18 @@ export class ImapFlow extends EventEmitter {
|
|
|
1764
1850
|
uids = untagged.attributes[0].value;
|
|
1765
1851
|
}
|
|
1766
1852
|
let uidList = expandRange(uids);
|
|
1853
|
+
let earlier = tags.includes('EARLIER');
|
|
1854
|
+
// RFC 7162 section 3.2.10: unlike VANISHED (EARLIER), a plain VANISHED reports messages the
|
|
1855
|
+
// client knows about and decrements the message count like the same number of EXPUNGEs would
|
|
1856
|
+
if (!earlier) {
|
|
1857
|
+
mailbox.exists = Math.max(0, mailbox.exists - uidList.length);
|
|
1858
|
+
}
|
|
1767
1859
|
for (let uid of uidList) {
|
|
1768
1860
|
let payload = {
|
|
1769
1861
|
path: mailbox.path,
|
|
1770
1862
|
uid,
|
|
1771
1863
|
vanished: true,
|
|
1772
|
-
earlier
|
|
1864
|
+
earlier
|
|
1773
1865
|
};
|
|
1774
1866
|
await this.notifyExpunge(payload);
|
|
1775
1867
|
}
|
|
@@ -1781,7 +1873,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1781
1873
|
// mailbox closed, ignore
|
|
1782
1874
|
return;
|
|
1783
1875
|
}
|
|
1784
|
-
let message = await formatMessageResponse(untagged, mailbox, this.idHashAlgorithm);
|
|
1876
|
+
let message = await formatMessageResponse(untagged, mailbox, this.idHashAlgorithm, this);
|
|
1785
1877
|
if (message.flags) {
|
|
1786
1878
|
let updateEvent = {
|
|
1787
1879
|
path: mailbox.path,
|
|
@@ -1797,7 +1889,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1797
1889
|
if (message.flagColor) {
|
|
1798
1890
|
updateEvent.flagColor = message.flagColor;
|
|
1799
1891
|
}
|
|
1800
|
-
this
|
|
1892
|
+
emitSafe(this, 'flags', updateEvent);
|
|
1801
1893
|
}
|
|
1802
1894
|
}
|
|
1803
1895
|
/** @internal */
|
|
@@ -1810,6 +1902,12 @@ export class ImapFlow extends EventEmitter {
|
|
|
1810
1902
|
}
|
|
1811
1903
|
return true;
|
|
1812
1904
|
}
|
|
1905
|
+
// servername for tls.connect(), left out for an IP literal host (this.servername is false then):
|
|
1906
|
+
// Node treats a false value like a missing one, but Bun throws a TypeError for it
|
|
1907
|
+
/** @internal */
|
|
1908
|
+
tlsServername() {
|
|
1909
|
+
return this.servername ? { servername: this.servername } : {};
|
|
1910
|
+
}
|
|
1813
1911
|
// Normalizes a message range from various input formats into an IMAP-compatible
|
|
1814
1912
|
// sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
|
|
1815
1913
|
// {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
|
|
@@ -1852,6 +1950,11 @@ export class ImapFlow extends EventEmitter {
|
|
|
1852
1950
|
if (!value) {
|
|
1853
1951
|
return false;
|
|
1854
1952
|
}
|
|
1953
|
+
// An empty mailbox has no message numbers: every sequence set, "1:*" included, would get a
|
|
1954
|
+
// BAD (RFC 9051 section 9, seq-number). UID sets may point past the end, so they are sent.
|
|
1955
|
+
if (!options.uid && this.mailbox && !this.mailbox.exists) {
|
|
1956
|
+
return false;
|
|
1957
|
+
}
|
|
1855
1958
|
return value;
|
|
1856
1959
|
}
|
|
1857
1960
|
// The single definition of "the connection is not free". A held or queued mailbox lock, a
|
|
@@ -1921,9 +2024,8 @@ export class ImapFlow extends EventEmitter {
|
|
|
1921
2024
|
let connector = this.secureConnection ? tls : net;
|
|
1922
2025
|
let opts = Object.assign({
|
|
1923
2026
|
host: this.host,
|
|
1924
|
-
servername: this.servername,
|
|
1925
2027
|
port: this.port
|
|
1926
|
-
}, this.options.tls || {});
|
|
2028
|
+
}, this.tlsServername(), this.options.tls || {});
|
|
1927
2029
|
this.untaggedHandlers.OK = (...args) => this.initialOK(...args);
|
|
1928
2030
|
this.untaggedHandlers.BYE = (...args) => this.serverBye(...args);
|
|
1929
2031
|
this.untaggedHandlers.PREAUTH = () => this.initialPREAUTH();
|
|
@@ -1981,6 +2083,12 @@ export class ImapFlow extends EventEmitter {
|
|
|
1981
2083
|
reject(err);
|
|
1982
2084
|
}, deadline.remaining());
|
|
1983
2085
|
let onConnect = () => {
|
|
2086
|
+
// close() may have run between the socket assignment and this callback (the
|
|
2087
|
+
// cleartext proxy path defers it): connect() is already rejected and the socket
|
|
2088
|
+
// gone, so arming the greeting timer now would only fire a stray GREETING_TIMEOUT
|
|
2089
|
+
if (this.isClosed || !this.socket) {
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
1984
2092
|
try {
|
|
1985
2093
|
clearTimer(this.connectTimeout);
|
|
1986
2094
|
// ImapFlow now owns the socket; drop the proxy's early error handler
|
|
@@ -2115,9 +2223,13 @@ export class ImapFlow extends EventEmitter {
|
|
|
2115
2223
|
this.idling = false;
|
|
2116
2224
|
this.closeConnectSteps();
|
|
2117
2225
|
if (typeof this.preCheck === 'function') {
|
|
2118
|
-
//
|
|
2119
|
-
//
|
|
2120
|
-
|
|
2226
|
+
// Taken off the connection first: breaking IDLE writes DONE, a write on a dead
|
|
2227
|
+
// transport calls close(), and close() must not find this function here and
|
|
2228
|
+
// re-enter it. The rejection this sees is almost always the NoConnection close()
|
|
2229
|
+
// is about to raise itself.
|
|
2230
|
+
let preCheck = this.preCheck;
|
|
2231
|
+
this.preCheck = false;
|
|
2232
|
+
preCheck().catch(err => logConnectionError(this, 'Failed to break IDLE while closing', err));
|
|
2121
2233
|
}
|
|
2122
2234
|
// Session-only public state must not survive the connection it describes: callers read
|
|
2123
2235
|
// these properties in reconnect logic and would otherwise mistake cached objects for
|
|
@@ -2237,6 +2349,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
2237
2349
|
closeRequests() {
|
|
2238
2350
|
// Collect all pending requests to reject
|
|
2239
2351
|
let pendingRequests = [];
|
|
2352
|
+
this.commandParts = [];
|
|
2240
2353
|
// reject command that is currently processed
|
|
2241
2354
|
if (this.currentRequest && this.requestTagMap.has(this.currentRequest.tag)) {
|
|
2242
2355
|
let tag = this.currentRequest.tag;
|
|
@@ -3095,7 +3208,34 @@ export class ImapFlow extends EventEmitter {
|
|
|
3095
3208
|
throw this.createNoConnectionError(false, { rejectedFrom: 'noSocket', command });
|
|
3096
3209
|
}
|
|
3097
3210
|
let handler = this.commands.get(command);
|
|
3098
|
-
|
|
3211
|
+
try {
|
|
3212
|
+
return await handler(this, ...args);
|
|
3213
|
+
}
|
|
3214
|
+
finally {
|
|
3215
|
+
this.releaseOrphanedResponse(command);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* Safety net for the `next()` contract of settleRequest(): a command handler that throws or
|
|
3220
|
+
* returns between the resolution of its exec() and the `next()` call would leave the reader
|
|
3221
|
+
* loop parked, stalling every later command until the socket timeout. Checked one macrotask
|
|
3222
|
+
* after the handler settled, so a concurrently running handler whose response has just been
|
|
3223
|
+
* resolved gets to release it itself first.
|
|
3224
|
+
*
|
|
3225
|
+
* @param command Command name, for the log entry.
|
|
3226
|
+
* @internal
|
|
3227
|
+
*/
|
|
3228
|
+
releaseOrphanedResponse(command) {
|
|
3229
|
+
let release = this.parkedRelease;
|
|
3230
|
+
if (!release) {
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
setImmediate(() => {
|
|
3234
|
+
if (this.parkedRelease === release) {
|
|
3235
|
+
this.log.warn({ msg: 'Command handler did not release its response', command, cid: this.id });
|
|
3236
|
+
release();
|
|
3237
|
+
}
|
|
3238
|
+
});
|
|
3099
3239
|
}
|
|
3100
3240
|
// Mailbox lock queue processor. Implements a mutex pattern: only one lock
|
|
3101
3241
|
// is active at a time. When the active lock is released, the next queued
|
|
@@ -3231,6 +3371,11 @@ export class ImapFlow extends EventEmitter {
|
|
|
3231
3371
|
try {
|
|
3232
3372
|
// Need to SELECT/EXAMINE a different mailbox
|
|
3233
3373
|
await this.mailboxOpen(path, options);
|
|
3374
|
+
if (!this.mailbox) {
|
|
3375
|
+
// mailboxOpen() resolves with nothing when the connection is no longer
|
|
3376
|
+
// authenticated (a BYE arrived meanwhile): no mailbox, so no lock on it
|
|
3377
|
+
throw this.createNoConnectionError(false, { rejectedFrom: 'mailboxLock', path });
|
|
3378
|
+
}
|
|
3234
3379
|
this.log.trace({
|
|
3235
3380
|
msg: 'Mailbox lock acquired [selected]',
|
|
3236
3381
|
path,
|
|
@@ -3341,35 +3486,36 @@ export class ImapFlow extends EventEmitter {
|
|
|
3341
3486
|
// {logger:false} never consults mainLogger, so it does not create the default logger
|
|
3342
3487
|
mainLogger = createConnectionLogger({ cid: this.id, logRaw: this.options.logRaw });
|
|
3343
3488
|
}
|
|
3489
|
+
// Whether the configured logger takes entries at this level: a disabled logger takes
|
|
3490
|
+
// none, a plain object of level methods the levels it has methods for, plus error and
|
|
3491
|
+
// fatal, which fall back to the console. The level methods and isLevelEnabled() below
|
|
3492
|
+
// share this one rule.
|
|
3493
|
+
const reachesLogger = (level) => this.options.logger !== false && (typeof mainLogger[level] === 'function' || level === 'error' || level === 'fatal');
|
|
3344
3494
|
let synteticLogger = {};
|
|
3345
3495
|
let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|
3346
3496
|
for (let level of levels) {
|
|
3347
3497
|
synteticLogger[level] = (...args) => {
|
|
3348
|
-
|
|
3349
|
-
if (this.options.logger !== false) {
|
|
3498
|
+
if (reachesLogger(level)) {
|
|
3350
3499
|
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
|
-
}
|
|
3500
|
+
if (typeof logMethod === 'function') {
|
|
3501
|
+
logMethod.apply(mainLogger, args);
|
|
3370
3502
|
}
|
|
3371
3503
|
else {
|
|
3372
|
-
|
|
3504
|
+
// error or fatal without a method of its own: the console, so it is never lost
|
|
3505
|
+
let entry = args[0];
|
|
3506
|
+
try {
|
|
3507
|
+
if (entry && typeof entry === 'object' && entry.err) {
|
|
3508
|
+
entry = Object.assign({}, entry, { err: flattenLoggedError(entry.err) });
|
|
3509
|
+
}
|
|
3510
|
+
console.error(JSON.stringify(entry));
|
|
3511
|
+
}
|
|
3512
|
+
catch {
|
|
3513
|
+
// Serializing failed (a circular structure, a BigInt, a throwing
|
|
3514
|
+
// getter). This fallback exists so an error is never lost, so hand
|
|
3515
|
+
// the entry to console.error itself - it inspects rather than
|
|
3516
|
+
// serializes, and handles all three - instead of dropping it.
|
|
3517
|
+
console.error(entry);
|
|
3518
|
+
}
|
|
3373
3519
|
}
|
|
3374
3520
|
}
|
|
3375
3521
|
if (this.emitLogs && args && args[0] && typeof args[0] === 'object') {
|
|
@@ -3392,8 +3538,23 @@ export class ImapFlow extends EventEmitter {
|
|
|
3392
3538
|
}
|
|
3393
3539
|
};
|
|
3394
3540
|
}
|
|
3541
|
+
// Whether an entry at this level reaches anyone, so the caller can skip building an
|
|
3542
|
+
// expensive one (a response serialized for the log) that nobody would see. 'log' events
|
|
3543
|
+
// carry every entry whatever the logger does with it, and a pino-like logger knows its
|
|
3544
|
+
// own threshold.
|
|
3545
|
+
synteticLogger.isLevelEnabled = (level) => this.emitLogs || (typeof mainLogger.isLevelEnabled === 'function' ? !!mainLogger.isLevelEnabled(level) : reachesLogger(level));
|
|
3395
3546
|
return synteticLogger;
|
|
3396
3547
|
}
|
|
3548
|
+
/**
|
|
3549
|
+
* Whether a log entry at this level reaches anyone, so the caller can skip building an
|
|
3550
|
+
* expensive one. A logger assigned to `log` from outside may lack the method, and then
|
|
3551
|
+
* every level counts as enabled.
|
|
3552
|
+
*
|
|
3553
|
+
* @internal
|
|
3554
|
+
*/
|
|
3555
|
+
isLogLevelEnabled(level) {
|
|
3556
|
+
return typeof this.log.isLevelEnabled === 'function' ? this.log.isLevelEnabled(level) : true;
|
|
3557
|
+
}
|
|
3397
3558
|
/**
|
|
3398
3559
|
* Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
|
|
3399
3560
|
* (e.g., STARTTLS) or transferring socket ownership. Call it while the connection is not
|
package/dist/esm/package-info.js
CHANGED