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/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
|
/**
|
|
@@ -1418,9 +1500,8 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1418
1500
|
// of the IP - accepting any "localhost" certificate for any IP-hosted
|
|
1419
1501
|
// server, and rejecting legitimate IP-SAN certificates.
|
|
1420
1502
|
host: this.host,
|
|
1421
|
-
servername: this.servername,
|
|
1422
1503
|
port: this.port
|
|
1423
|
-
}, this.options.tls || {});
|
|
1504
|
+
}, this.tlsServername(), this.options.tls || {});
|
|
1424
1505
|
this.clearSocketHandlers();
|
|
1425
1506
|
let settled = false;
|
|
1426
1507
|
// Single settlement path for the upgrade. Every terminal outcome - handshake
|
|
@@ -1613,7 +1694,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1613
1694
|
throw new errors_js_1.AuthenticationFailure('No matching authentication method');
|
|
1614
1695
|
}
|
|
1615
1696
|
/** @internal */
|
|
1616
|
-
beginSession(
|
|
1697
|
+
beginSession() {
|
|
1617
1698
|
(0, tools_js_1.clearTimer)(this.greetingTimeout);
|
|
1618
1699
|
this.greetingReceived = true;
|
|
1619
1700
|
this.untaggedHandlers.OK = null;
|
|
@@ -1632,15 +1713,21 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1632
1713
|
}
|
|
1633
1714
|
})
|
|
1634
1715
|
.catch(err => {
|
|
1635
|
-
|
|
1716
|
+
// The transport goes with the failed attempt: the instance can not be reused, and
|
|
1717
|
+
// an open socket would only trip the inactivity watchdog later
|
|
1718
|
+
this.closeAfter();
|
|
1636
1719
|
if (typeof this.initialReject === 'function') {
|
|
1720
|
+
this.log.error({ err, cid: this.id });
|
|
1637
1721
|
(0, tools_js_1.clearTimer)(this.greetingTimeout);
|
|
1638
1722
|
let reject = this.initialReject;
|
|
1639
1723
|
this.initialResolve = false;
|
|
1640
1724
|
this.initialReject = false;
|
|
1641
1725
|
return reject(err);
|
|
1642
1726
|
}
|
|
1643
|
-
|
|
1727
|
+
// connect() was already settled by whatever took the connection down (a socket
|
|
1728
|
+
// error or close rejected it first), so this failure is a consequence of that.
|
|
1729
|
+
// A second 'error' event for it would throw when no listener is attached.
|
|
1730
|
+
(0, tools_js_1.logConnectionError)(this, 'Session setup failed', err);
|
|
1644
1731
|
});
|
|
1645
1732
|
}
|
|
1646
1733
|
/** @internal */
|
|
@@ -1648,8 +1735,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1648
1735
|
this.greeting = (0, tools_js_1.getTextValues)(message.attributes)
|
|
1649
1736
|
.filter(entry => entry)
|
|
1650
1737
|
.join('');
|
|
1651
|
-
|
|
1652
|
-
this.beginSession(err => this.emitError(err));
|
|
1738
|
+
this.beginSession();
|
|
1653
1739
|
}
|
|
1654
1740
|
/** @internal */
|
|
1655
1741
|
async initialPREAUTH() {
|
|
@@ -1660,10 +1746,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1660
1746
|
// documented contract for the `authenticated` property: `true` when the
|
|
1661
1747
|
// connection was authenticated by a PREAUTH greeting (no credentials known)
|
|
1662
1748
|
this.authenticated = true;
|
|
1663
|
-
this.beginSession(
|
|
1664
|
-
this.log.error({ err, cid: this.id });
|
|
1665
|
-
this.closeAfter();
|
|
1666
|
-
});
|
|
1749
|
+
this.beginSession();
|
|
1667
1750
|
}
|
|
1668
1751
|
/** @internal */
|
|
1669
1752
|
async serverBye(parsed) {
|
|
@@ -1675,10 +1758,13 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1675
1758
|
this.byeReason = reason || 'Server closed connection';
|
|
1676
1759
|
this.untaggedHandlers.BYE = null;
|
|
1677
1760
|
this.state = this.states.LOGOUT;
|
|
1678
|
-
//
|
|
1679
|
-
//
|
|
1680
|
-
// timeout
|
|
1681
|
-
|
|
1761
|
+
// BYE means the server is about to close the connection (RFC 9051 7.1.5), so the socket
|
|
1762
|
+
// is not waited out: a BYE greeting kept open would leave connect() pending until the
|
|
1763
|
+
// greeting timeout, and an unsolicited BYE whose FIN never arrives would leave the
|
|
1764
|
+
// in-flight command and the queue behind it waiting for the socket watchdog. The BYE
|
|
1765
|
+
// that answers a LOGOUT is the exception: its tagged completion closes the connection.
|
|
1766
|
+
const answersLogout = !!this.currentRequest && this.currentRequest.command.toUpperCase() === 'LOGOUT';
|
|
1767
|
+
if (!this.greetingReceived || !answersLogout) {
|
|
1682
1768
|
this.closeAfter();
|
|
1683
1769
|
}
|
|
1684
1770
|
}
|
|
@@ -1740,7 +1826,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1740
1826
|
// keep exists up to date
|
|
1741
1827
|
let prevCount = this.mailbox.exists;
|
|
1742
1828
|
this.mailbox.exists = count;
|
|
1743
|
-
|
|
1829
|
+
(0, tools_js_1.emitSafe)(this, 'exists', {
|
|
1744
1830
|
path: this.mailbox.path,
|
|
1745
1831
|
count,
|
|
1746
1832
|
prevCount
|
|
@@ -1751,7 +1837,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1751
1837
|
/** @internal */
|
|
1752
1838
|
async notifyExpunge(payload) {
|
|
1753
1839
|
if (typeof this.options.expungeHandler !== 'function') {
|
|
1754
|
-
|
|
1840
|
+
(0, tools_js_1.emitSafe)(this, 'expunge', payload);
|
|
1755
1841
|
return;
|
|
1756
1842
|
}
|
|
1757
1843
|
try {
|
|
@@ -1805,12 +1891,18 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1805
1891
|
uids = untagged.attributes[0].value;
|
|
1806
1892
|
}
|
|
1807
1893
|
let uidList = (0, tools_js_1.expandRange)(uids);
|
|
1894
|
+
let earlier = tags.includes('EARLIER');
|
|
1895
|
+
// RFC 7162 section 3.2.10: unlike VANISHED (EARLIER), a plain VANISHED reports messages the
|
|
1896
|
+
// client knows about and decrements the message count like the same number of EXPUNGEs would
|
|
1897
|
+
if (!earlier) {
|
|
1898
|
+
mailbox.exists = Math.max(0, mailbox.exists - uidList.length);
|
|
1899
|
+
}
|
|
1808
1900
|
for (let uid of uidList) {
|
|
1809
1901
|
let payload = {
|
|
1810
1902
|
path: mailbox.path,
|
|
1811
1903
|
uid,
|
|
1812
1904
|
vanished: true,
|
|
1813
|
-
earlier
|
|
1905
|
+
earlier
|
|
1814
1906
|
};
|
|
1815
1907
|
await this.notifyExpunge(payload);
|
|
1816
1908
|
}
|
|
@@ -1822,7 +1914,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1822
1914
|
// mailbox closed, ignore
|
|
1823
1915
|
return;
|
|
1824
1916
|
}
|
|
1825
|
-
let message = await (0, tools_js_1.formatMessageResponse)(untagged, mailbox, this.idHashAlgorithm);
|
|
1917
|
+
let message = await (0, tools_js_1.formatMessageResponse)(untagged, mailbox, this.idHashAlgorithm, this);
|
|
1826
1918
|
if (message.flags) {
|
|
1827
1919
|
let updateEvent = {
|
|
1828
1920
|
path: mailbox.path,
|
|
@@ -1838,7 +1930,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1838
1930
|
if (message.flagColor) {
|
|
1839
1931
|
updateEvent.flagColor = message.flagColor;
|
|
1840
1932
|
}
|
|
1841
|
-
|
|
1933
|
+
(0, tools_js_1.emitSafe)(this, 'flags', updateEvent);
|
|
1842
1934
|
}
|
|
1843
1935
|
}
|
|
1844
1936
|
/** @internal */
|
|
@@ -1851,6 +1943,12 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1851
1943
|
}
|
|
1852
1944
|
return true;
|
|
1853
1945
|
}
|
|
1946
|
+
// servername for tls.connect(), left out for an IP literal host (this.servername is false then):
|
|
1947
|
+
// Node treats a false value like a missing one, but Bun throws a TypeError for it
|
|
1948
|
+
/** @internal */
|
|
1949
|
+
tlsServername() {
|
|
1950
|
+
return this.servername ? { servername: this.servername } : {};
|
|
1951
|
+
}
|
|
1854
1952
|
// Normalizes a message range from various input formats into an IMAP-compatible
|
|
1855
1953
|
// sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
|
|
1856
1954
|
// {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
|
|
@@ -1893,6 +1991,11 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1893
1991
|
if (!value) {
|
|
1894
1992
|
return false;
|
|
1895
1993
|
}
|
|
1994
|
+
// An empty mailbox has no message numbers: every sequence set, "1:*" included, would get a
|
|
1995
|
+
// BAD (RFC 9051 section 9, seq-number). UID sets may point past the end, so they are sent.
|
|
1996
|
+
if (!options.uid && this.mailbox && !this.mailbox.exists) {
|
|
1997
|
+
return false;
|
|
1998
|
+
}
|
|
1896
1999
|
return value;
|
|
1897
2000
|
}
|
|
1898
2001
|
// The single definition of "the connection is not free". A held or queued mailbox lock, a
|
|
@@ -1962,9 +2065,8 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1962
2065
|
let connector = this.secureConnection ? node_tls_1.default : node_net_1.default;
|
|
1963
2066
|
let opts = Object.assign({
|
|
1964
2067
|
host: this.host,
|
|
1965
|
-
servername: this.servername,
|
|
1966
2068
|
port: this.port
|
|
1967
|
-
}, this.options.tls || {});
|
|
2069
|
+
}, this.tlsServername(), this.options.tls || {});
|
|
1968
2070
|
this.untaggedHandlers.OK = (...args) => this.initialOK(...args);
|
|
1969
2071
|
this.untaggedHandlers.BYE = (...args) => this.serverBye(...args);
|
|
1970
2072
|
this.untaggedHandlers.PREAUTH = () => this.initialPREAUTH();
|
|
@@ -2022,6 +2124,12 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2022
2124
|
reject(err);
|
|
2023
2125
|
}, deadline.remaining());
|
|
2024
2126
|
let onConnect = () => {
|
|
2127
|
+
// close() may have run between the socket assignment and this callback (the
|
|
2128
|
+
// cleartext proxy path defers it): connect() is already rejected and the socket
|
|
2129
|
+
// gone, so arming the greeting timer now would only fire a stray GREETING_TIMEOUT
|
|
2130
|
+
if (this.isClosed || !this.socket) {
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2025
2133
|
try {
|
|
2026
2134
|
(0, tools_js_1.clearTimer)(this.connectTimeout);
|
|
2027
2135
|
// ImapFlow now owns the socket; drop the proxy's early error handler
|
|
@@ -2156,9 +2264,13 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2156
2264
|
this.idling = false;
|
|
2157
2265
|
this.closeConnectSteps();
|
|
2158
2266
|
if (typeof this.preCheck === 'function') {
|
|
2159
|
-
//
|
|
2160
|
-
//
|
|
2161
|
-
|
|
2267
|
+
// Taken off the connection first: breaking IDLE writes DONE, a write on a dead
|
|
2268
|
+
// transport calls close(), and close() must not find this function here and
|
|
2269
|
+
// re-enter it. The rejection this sees is almost always the NoConnection close()
|
|
2270
|
+
// is about to raise itself.
|
|
2271
|
+
let preCheck = this.preCheck;
|
|
2272
|
+
this.preCheck = false;
|
|
2273
|
+
preCheck().catch(err => (0, tools_js_1.logConnectionError)(this, 'Failed to break IDLE while closing', err));
|
|
2162
2274
|
}
|
|
2163
2275
|
// Session-only public state must not survive the connection it describes: callers read
|
|
2164
2276
|
// these properties in reconnect logic and would otherwise mistake cached objects for
|
|
@@ -2278,6 +2390,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2278
2390
|
closeRequests() {
|
|
2279
2391
|
// Collect all pending requests to reject
|
|
2280
2392
|
let pendingRequests = [];
|
|
2393
|
+
this.commandParts = [];
|
|
2281
2394
|
// reject command that is currently processed
|
|
2282
2395
|
if (this.currentRequest && this.requestTagMap.has(this.currentRequest.tag)) {
|
|
2283
2396
|
let tag = this.currentRequest.tag;
|
|
@@ -3136,7 +3249,34 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3136
3249
|
throw this.createNoConnectionError(false, { rejectedFrom: 'noSocket', command });
|
|
3137
3250
|
}
|
|
3138
3251
|
let handler = this.commands.get(command);
|
|
3139
|
-
|
|
3252
|
+
try {
|
|
3253
|
+
return await handler(this, ...args);
|
|
3254
|
+
}
|
|
3255
|
+
finally {
|
|
3256
|
+
this.releaseOrphanedResponse(command);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
/**
|
|
3260
|
+
* Safety net for the `next()` contract of settleRequest(): a command handler that throws or
|
|
3261
|
+
* returns between the resolution of its exec() and the `next()` call would leave the reader
|
|
3262
|
+
* loop parked, stalling every later command until the socket timeout. Checked one macrotask
|
|
3263
|
+
* after the handler settled, so a concurrently running handler whose response has just been
|
|
3264
|
+
* resolved gets to release it itself first.
|
|
3265
|
+
*
|
|
3266
|
+
* @param command Command name, for the log entry.
|
|
3267
|
+
* @internal
|
|
3268
|
+
*/
|
|
3269
|
+
releaseOrphanedResponse(command) {
|
|
3270
|
+
let release = this.parkedRelease;
|
|
3271
|
+
if (!release) {
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
setImmediate(() => {
|
|
3275
|
+
if (this.parkedRelease === release) {
|
|
3276
|
+
this.log.warn({ msg: 'Command handler did not release its response', command, cid: this.id });
|
|
3277
|
+
release();
|
|
3278
|
+
}
|
|
3279
|
+
});
|
|
3140
3280
|
}
|
|
3141
3281
|
// Mailbox lock queue processor. Implements a mutex pattern: only one lock
|
|
3142
3282
|
// is active at a time. When the active lock is released, the next queued
|
|
@@ -3272,6 +3412,11 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3272
3412
|
try {
|
|
3273
3413
|
// Need to SELECT/EXAMINE a different mailbox
|
|
3274
3414
|
await this.mailboxOpen(path, options);
|
|
3415
|
+
if (!this.mailbox) {
|
|
3416
|
+
// mailboxOpen() resolves with nothing when the connection is no longer
|
|
3417
|
+
// authenticated (a BYE arrived meanwhile): no mailbox, so no lock on it
|
|
3418
|
+
throw this.createNoConnectionError(false, { rejectedFrom: 'mailboxLock', path });
|
|
3419
|
+
}
|
|
3275
3420
|
this.log.trace({
|
|
3276
3421
|
msg: 'Mailbox lock acquired [selected]',
|
|
3277
3422
|
path,
|
|
@@ -3382,35 +3527,36 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3382
3527
|
// {logger:false} never consults mainLogger, so it does not create the default logger
|
|
3383
3528
|
mainLogger = (0, logger_js_1.createConnectionLogger)({ cid: this.id, logRaw: this.options.logRaw });
|
|
3384
3529
|
}
|
|
3530
|
+
// Whether the configured logger takes entries at this level: a disabled logger takes
|
|
3531
|
+
// none, a plain object of level methods the levels it has methods for, plus error and
|
|
3532
|
+
// fatal, which fall back to the console. The level methods and isLevelEnabled() below
|
|
3533
|
+
// share this one rule.
|
|
3534
|
+
const reachesLogger = (level) => this.options.logger !== false && (typeof mainLogger[level] === 'function' || level === 'error' || level === 'fatal');
|
|
3385
3535
|
let synteticLogger = {};
|
|
3386
3536
|
let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|
3387
3537
|
for (let level of levels) {
|
|
3388
3538
|
synteticLogger[level] = (...args) => {
|
|
3389
|
-
|
|
3390
|
-
if (this.options.logger !== false) {
|
|
3539
|
+
if (reachesLogger(level)) {
|
|
3391
3540
|
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
|
-
}
|
|
3541
|
+
if (typeof logMethod === 'function') {
|
|
3542
|
+
logMethod.apply(mainLogger, args);
|
|
3411
3543
|
}
|
|
3412
3544
|
else {
|
|
3413
|
-
|
|
3545
|
+
// error or fatal without a method of its own: the console, so it is never lost
|
|
3546
|
+
let entry = args[0];
|
|
3547
|
+
try {
|
|
3548
|
+
if (entry && typeof entry === 'object' && entry.err) {
|
|
3549
|
+
entry = Object.assign({}, entry, { err: flattenLoggedError(entry.err) });
|
|
3550
|
+
}
|
|
3551
|
+
console.error(JSON.stringify(entry));
|
|
3552
|
+
}
|
|
3553
|
+
catch {
|
|
3554
|
+
// Serializing failed (a circular structure, a BigInt, a throwing
|
|
3555
|
+
// getter). This fallback exists so an error is never lost, so hand
|
|
3556
|
+
// the entry to console.error itself - it inspects rather than
|
|
3557
|
+
// serializes, and handles all three - instead of dropping it.
|
|
3558
|
+
console.error(entry);
|
|
3559
|
+
}
|
|
3414
3560
|
}
|
|
3415
3561
|
}
|
|
3416
3562
|
if (this.emitLogs && args && args[0] && typeof args[0] === 'object') {
|
|
@@ -3433,8 +3579,23 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3433
3579
|
}
|
|
3434
3580
|
};
|
|
3435
3581
|
}
|
|
3582
|
+
// Whether an entry at this level reaches anyone, so the caller can skip building an
|
|
3583
|
+
// expensive one (a response serialized for the log) that nobody would see. 'log' events
|
|
3584
|
+
// carry every entry whatever the logger does with it, and a pino-like logger knows its
|
|
3585
|
+
// own threshold.
|
|
3586
|
+
synteticLogger.isLevelEnabled = (level) => this.emitLogs || (typeof mainLogger.isLevelEnabled === 'function' ? !!mainLogger.isLevelEnabled(level) : reachesLogger(level));
|
|
3436
3587
|
return synteticLogger;
|
|
3437
3588
|
}
|
|
3589
|
+
/**
|
|
3590
|
+
* Whether a log entry at this level reaches anyone, so the caller can skip building an
|
|
3591
|
+
* expensive one. A logger assigned to `log` from outside may lack the method, and then
|
|
3592
|
+
* every level counts as enabled.
|
|
3593
|
+
*
|
|
3594
|
+
* @internal
|
|
3595
|
+
*/
|
|
3596
|
+
isLogLevelEnabled(level) {
|
|
3597
|
+
return typeof this.log.isLevelEnabled === 'function' ? this.log.isLevelEnabled(level) : true;
|
|
3598
|
+
}
|
|
3438
3599
|
/**
|
|
3439
3600
|
* Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
|
|
3440
3601
|
* (e.g., STARTTLS) or transferring socket ownership. Call it while the connection is not
|
package/dist/cjs/package-info.js
CHANGED