imapflow 1.6.6 → 1.7.1
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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +20 -0
- package/README.md +2 -3
- package/lib/commands/authenticate.js +15 -5
- package/lib/commands/idle.js +46 -18
- package/lib/imap-flow.d.ts +15 -1
- package/lib/imap-flow.js +373 -92
- package/lib/proxy-connection.js +14 -4
- package/lib/tools.js +29 -0
- package/package.json +3 -3
- package/test/auto-idle-test.js +470 -0
- package/test/connection-edge-cases-test.js +3 -1
- package/test/fixtures/test-client.js +57 -0
- package/test/idle-polling-test.js +88 -0
- package/test/imap-flow-coverage-test.js +4 -10
- package/test/imap-flow-fetch-download-test.js +3 -10
- package/test/imap-flow-internals-test.js +168 -50
- package/test/timer-policy-test.js +4 -17
package/lib/imap-flow.js
CHANGED
|
@@ -38,6 +38,7 @@ const {
|
|
|
38
38
|
AuthenticationFailure,
|
|
39
39
|
getColorFlags,
|
|
40
40
|
hasCapability,
|
|
41
|
+
logConnectionError,
|
|
41
42
|
unrefTimer,
|
|
42
43
|
parseUintValue,
|
|
43
44
|
isUnsafeKey,
|
|
@@ -64,6 +65,91 @@ const MAX_THROTTLE_DELAY = 5 * 60 * 1000;
|
|
|
64
65
|
// ImapFlow constructor option `maxLockHoldTime`. Set to 0 or false to disable.
|
|
65
66
|
const HELD_LOCK_WARN_MS = 30 * 60 * 1000;
|
|
66
67
|
|
|
68
|
+
// How long the connection has to stay inactive before auto-IDLE starts. Long enough that a caller
|
|
69
|
+
// running a sequence of commands is not interrupted by an IDLE it immediately has to break.
|
|
70
|
+
// Configurable via the ImapFlow constructor option `autoIdleDelay`.
|
|
71
|
+
const AUTO_IDLE_DELAY = 15 * 1000;
|
|
72
|
+
|
|
73
|
+
// Headroom kept between the auto-IDLE delay and the socket inactivity watchdog, so IDLE reaches
|
|
74
|
+
// the wire before the watchdog can fire. See normalizeAutoIdleDelay().
|
|
75
|
+
const AUTO_IDLE_SOCKET_MARGIN = 1000;
|
|
76
|
+
|
|
77
|
+
// Commands whose client frames carry credentials; the raw traffic log withholds frame content
|
|
78
|
+
// while one of these is in flight. See the logRaw branch in write().
|
|
79
|
+
const RAW_SENSITIVE_COMMANDS = new Set(['LOGIN', 'AUTHENTICATE']);
|
|
80
|
+
|
|
81
|
+
// Stand-in payload for a withheld raw client frame. Fixed width, so the entry says nothing
|
|
82
|
+
// about the length of what it replaced.
|
|
83
|
+
const RAW_HIDDEN_PLACEHOLDER = Buffer.from('(* value hidden *)\r\n').toString('base64');
|
|
84
|
+
|
|
85
|
+
// Whether any attribute of a command is marked as a secret. Recurses into nested lists because
|
|
86
|
+
// the command compiler honors `sensitive` at any depth, and the two must agree on what counts.
|
|
87
|
+
function hasSensitiveAttribute(attributes) {
|
|
88
|
+
return [].concat(attributes || []).some(node => (Array.isArray(node) ? hasSensitiveAttribute(node) : !!node && node.sensitive));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// How deep flattenLoggedError() follows a chain of errors. Bounded because the chain comes from
|
|
92
|
+
// whatever failed, not from this library: a cause chain can be arbitrarily long, and the cycle
|
|
93
|
+
// check below only catches errors that repeat.
|
|
94
|
+
const MAX_ERROR_FLATTEN_DEPTH = 4;
|
|
95
|
+
|
|
96
|
+
// Recognizes an Error without instanceof, which fails for an error that crossed a realm boundary
|
|
97
|
+
// (worker thread, vm context) even though it serializes exactly the same way.
|
|
98
|
+
function isErrorLike(value) {
|
|
99
|
+
return value instanceof Error || (!!value && typeof value === 'object' && typeof value.message === 'string' && typeof value.stack === 'string');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// An Error carries `message` and `stack` on its prototype rather than as own enumerable
|
|
103
|
+
// properties, so JSON.stringify() renders one as `{}` and both logger fallback paths (the console
|
|
104
|
+
// fallback and emitLogs) would drop everything identifying it. Flattening happens here for both,
|
|
105
|
+
// so their shapes cannot drift apart.
|
|
106
|
+
//
|
|
107
|
+
// Nested errors are flattened too, because the top level is often not where the answer is: this
|
|
108
|
+
// library attaches the underlying failure as an enumerable `_err` (proxy setup, response
|
|
109
|
+
// processing, normalized connection deadlines), and Node reports a multi-address connect failure
|
|
110
|
+
// as an AggregateError whose members hold the per-address causes.
|
|
111
|
+
function flattenLoggedError(value, depth = 0, seen = new Set()) {
|
|
112
|
+
if (depth >= MAX_ERROR_FLATTEN_DEPTH) {
|
|
113
|
+
return isErrorLike(value) ? value.message : value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
return value.map(entry => flattenLoggedError(entry, depth + 1, seen));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!isErrorLike(value)) {
|
|
121
|
+
// Anything else is left alone: exploding a Buffer would produce one key per byte, and a
|
|
122
|
+
// Date would become a pair of undefined fields.
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// A repeat renders as its message alone, so a chain that loops back does not restate a full
|
|
127
|
+
// stack for every level down to the depth cap
|
|
128
|
+
if (seen.has(value)) {
|
|
129
|
+
return value.message;
|
|
130
|
+
}
|
|
131
|
+
seen.add(value);
|
|
132
|
+
|
|
133
|
+
let flatErr = {
|
|
134
|
+
message: value.message,
|
|
135
|
+
stack: value.stack
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// `cause` (passed through the Error options argument) and the AggregateError members are own
|
|
139
|
+
// properties but not enumerable, so Object.keys does not list them
|
|
140
|
+
for (let key of new Set([...Object.keys(value), 'cause', 'errors'])) {
|
|
141
|
+
if (key in value) {
|
|
142
|
+
flatErr[key] = flattenLoggedError(value[key], depth + 1, seen);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return flatErr;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The largest delay setTimeout can honor (2^31 - 1 ms). Anything above fires after 1 ms instead,
|
|
150
|
+
// so the auto-IDLE delay cap has to stay inside this range even when socketTimeout is not.
|
|
151
|
+
const MAX_TIMER_DELAY = 2 ** 31 - 1;
|
|
152
|
+
|
|
67
153
|
const states = {
|
|
68
154
|
NOT_AUTHENTICATED: 0x01,
|
|
69
155
|
AUTHENTICATED: 0x02,
|
|
@@ -71,6 +157,49 @@ const states = {
|
|
|
71
157
|
LOGOUT: 0x04
|
|
72
158
|
};
|
|
73
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Normalizes the configured auto-IDLE delay into a value `setTimeout` can honor. Anything Node
|
|
162
|
+
* would silently turn into a 1ms timer - NaN, a negative number, a value above the 32-bit range -
|
|
163
|
+
* falls back to the default instead, because a 1ms delay means an IDLE/DONE round trip around
|
|
164
|
+
* every single command. The delay is also capped below `socketTimeout`, see AUTO_IDLE_SOCKET_MARGIN.
|
|
165
|
+
*
|
|
166
|
+
* @param {*} value - The configured `autoIdleDelay` option.
|
|
167
|
+
* @param {Number} socketTimeout - The normalized socket inactivity timeout.
|
|
168
|
+
* @param {Object} log - Logger, used to report a value that could not be used as given.
|
|
169
|
+
* @param {String} cid - Connection id for the log entry.
|
|
170
|
+
* @returns {Number} Delay in milliseconds.
|
|
171
|
+
*/
|
|
172
|
+
const normalizeAutoIdleDelay = (value, socketTimeout, log, cid) => {
|
|
173
|
+
const maxDelay = Math.max(0, Math.min(socketTimeout, MAX_TIMER_DELAY) - AUTO_IDLE_SOCKET_MARGIN);
|
|
174
|
+
const configured = value !== undefined && value !== null;
|
|
175
|
+
|
|
176
|
+
// Numeric strings are accepted, because configuration usually arrives from an environment
|
|
177
|
+
// variable or a JSON file. Booleans and blank strings are not: Number() would read them as 0,
|
|
178
|
+
// i.e. "IDLE around every command", the opposite of the "off" they suggest.
|
|
179
|
+
let delay = typeof value === 'number' || (typeof value === 'string' && value.trim()) ? Number(value) : NaN;
|
|
180
|
+
let reason = null;
|
|
181
|
+
|
|
182
|
+
if (!Number.isFinite(delay) || delay < 0) {
|
|
183
|
+
reason = 'not a non-negative finite number';
|
|
184
|
+
delay = AUTO_IDLE_DELAY;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (delay > maxDelay) {
|
|
188
|
+
// An invalid value keeps its own reason: the cap then applies to the fallback default,
|
|
189
|
+
// not to anything the caller asked for.
|
|
190
|
+
reason = reason || `above socketTimeout (${socketTimeout} ms)`;
|
|
191
|
+
delay = maxDelay;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Only an explicitly configured value is worth warning about. Capping the default because the
|
|
195
|
+
// caller picked a short socketTimeout is expected behavior, not a misconfiguration.
|
|
196
|
+
if (configured && reason) {
|
|
197
|
+
log.warn({ msg: 'Adjusted unusable autoIdleDelay option', requested: value, autoIdleDelay: delay, reason, cid });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return Math.floor(delay);
|
|
201
|
+
};
|
|
202
|
+
|
|
74
203
|
/**
|
|
75
204
|
* @typedef {Object} MailboxObject
|
|
76
205
|
* @global
|
|
@@ -188,6 +317,15 @@ class ImapFlow extends EventEmitter {
|
|
|
188
317
|
* @property {Boolean} [disableAutoIdle=false]
|
|
189
318
|
* If `true`, do not start IDLE automatically. Useful when only specific operations are needed.
|
|
190
319
|
*
|
|
320
|
+
* @property {Number} [autoIdleDelay=15000]
|
|
321
|
+
* How long (in milliseconds) the connection has to be inactive before IDLE is started automatically.
|
|
322
|
+
* Keep it above the pause your own code usually leaves between two commands, otherwise every command is
|
|
323
|
+
* followed by an IDLE that the next command has to break, costing two extra round-trips per command.
|
|
324
|
+
* To turn auto-IDLE off entirely use `disableAutoIdle` rather than a very large delay: the value is
|
|
325
|
+
* capped below `socketTimeout`, because auto-IDLE has to start before the inactivity watchdog fires.
|
|
326
|
+
* On servers without IDLE support this controls when the polling fallback starts, not how often it
|
|
327
|
+
* polls - the poll interval is `maxIdleTime`, capped at 2 minutes.
|
|
328
|
+
*
|
|
191
329
|
* @property {Object} [tls]
|
|
192
330
|
* Additional TLS options. For details, see [Node.js TLS connect](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback).
|
|
193
331
|
*
|
|
@@ -206,6 +344,7 @@ class ImapFlow extends EventEmitter {
|
|
|
206
344
|
*
|
|
207
345
|
* @property {Boolean} [logRaw=false]
|
|
208
346
|
* If `true`, logs all raw data (read and written) in base64 encoding. You can pipe such logs to [eerawlog](https://github.com/postalsys/eerawlog) command for readable output.
|
|
347
|
+
* Client frames that carry credentials are replaced with a fixed placeholder and the entry is marked with `hidden: true`.
|
|
209
348
|
*
|
|
210
349
|
* @property {Boolean} [emitLogs=false]
|
|
211
350
|
* If `true`, emits `'log'` events with the same data passed to the logger.
|
|
@@ -373,6 +512,13 @@ class ImapFlow extends EventEmitter {
|
|
|
373
512
|
|
|
374
513
|
this.commandParts = [];
|
|
375
514
|
|
|
515
|
+
// Whether the command currently being written carries credentials. send() sets this for
|
|
516
|
+
// every command before its first frame reaches the socket, and the raw traffic log reads
|
|
517
|
+
// it; every write belongs to the command send() dispatched last, because trySend() keeps
|
|
518
|
+
// one command in flight at a time. The initial value only covers a write before the
|
|
519
|
+
// first command, which no current path performs. See write().
|
|
520
|
+
this.rawSensitiveCommand = true;
|
|
521
|
+
|
|
376
522
|
/**
|
|
377
523
|
* Active IMAP capabilities. Value is either `true` for toggleable capabilities (eg. `UIDPLUS`)
|
|
378
524
|
* or a number for capabilities with a value (eg. `APPENDLIMIT`)
|
|
@@ -438,6 +584,14 @@ class ImapFlow extends EventEmitter {
|
|
|
438
584
|
this.idRequested = false;
|
|
439
585
|
|
|
440
586
|
this.maxIdleTime = this.options.maxIdleTime || false;
|
|
587
|
+
this.autoIdleDelay = normalizeAutoIdleDelay(this.options.autoIdleDelay, this.socketTimeout, this.log, this.id);
|
|
588
|
+
|
|
589
|
+
// Wall-clock time of the last fallback poll, owned by lib/commands/idle.js
|
|
590
|
+
this._lastPollAt = 0;
|
|
591
|
+
|
|
592
|
+
// Download streams still fetching chunks. Counted, not a flag, so overlapping downloads
|
|
593
|
+
// cannot clear each other's suppression of auto-IDLE.
|
|
594
|
+
this._openDownloads = 0;
|
|
441
595
|
this.missingIdleCommand = (this.options.missingIdleCommand || '').toString().toUpperCase().trim() || 'NOOP';
|
|
442
596
|
|
|
443
597
|
this.disableBinary = !!this.options.disableBinary;
|
|
@@ -559,10 +713,17 @@ class ImapFlow extends EventEmitter {
|
|
|
559
713
|
}
|
|
560
714
|
|
|
561
715
|
if (this.logRaw) {
|
|
716
|
+
// Client frames of an authentication exchange carry credentials: the LOGIN
|
|
717
|
+
// arguments, and for AUTHENTICATE also the continuation writes (SASL PLAIN
|
|
718
|
+
// response, AUTH=LOGIN password, OAuth token payload) that bypass send(). The
|
|
719
|
+
// parsed command log masks these, so the raw log must withhold them too, but
|
|
720
|
+
// `data` still carries the placeholder rather than being dropped - the field is
|
|
721
|
+
// part of the documented log format and consumers decode it unconditionally.
|
|
562
722
|
this.log.trace({
|
|
563
723
|
src: 'c',
|
|
564
724
|
msg: 'write to socket',
|
|
565
|
-
data: chunk.toString('base64'),
|
|
725
|
+
data: this.rawSensitiveCommand ? RAW_HIDDEN_PLACEHOLDER : chunk.toString('base64'),
|
|
726
|
+
...(this.rawSensitiveCommand ? { hidden: true } : {}),
|
|
566
727
|
compress: !!this._deflate,
|
|
567
728
|
secure: !!this.secureConnection,
|
|
568
729
|
cid: this.id
|
|
@@ -618,6 +779,19 @@ class ImapFlow extends EventEmitter {
|
|
|
618
779
|
return;
|
|
619
780
|
}
|
|
620
781
|
|
|
782
|
+
// Classify before the first await. Every frame of this command - the command line and
|
|
783
|
+
// any continuation write that follows it - belongs to it until the next send(), because
|
|
784
|
+
// trySend() keeps one command in flight at a time. Reading currentRequest inside write()
|
|
785
|
+
// instead would be racy: rejectCurrentRequest() can clear it while the two compiler
|
|
786
|
+
// awaits below are pending, and the credential frame would then be logged in the clear.
|
|
787
|
+
// Uppercased because the wire protocol is case-insensitive and exec() passes the
|
|
788
|
+
// caller's spelling through unchanged. The command list covers the mechanisms whose
|
|
789
|
+
// secret arrives in a continuation frame, which carries no attributes of its own; the
|
|
790
|
+
// `sensitive` marker catches anything that instead puts a secret on the command line,
|
|
791
|
+
// so marking an attribute is enough to keep a new command out of the raw log too.
|
|
792
|
+
this.rawSensitiveCommand =
|
|
793
|
+
RAW_SENSITIVE_COMMANDS.has(typeof data.command === 'string' ? data.command.toUpperCase() : '') || hasSensitiveAttribute(data.attributes);
|
|
794
|
+
|
|
621
795
|
// Compile with asArray=true: splits output into parts for literal handling.
|
|
622
796
|
// First part is the command text up to the first literal, remaining parts
|
|
623
797
|
// are stored in this.commandParts and sent after server "+" continuations.
|
|
@@ -715,7 +889,7 @@ class ImapFlow extends EventEmitter {
|
|
|
715
889
|
// trySend() settles dispatch failures itself, by rejecting the affected
|
|
716
890
|
// command through requestTagMap; this catch exists only so a throw from the
|
|
717
891
|
// dispatch machinery itself can never surface as a floating rejection.
|
|
718
|
-
this.trySend().catch(err => this
|
|
892
|
+
this.trySend().catch(err => logConnectionError(this, 'Failed to dispatch command', err));
|
|
719
893
|
});
|
|
720
894
|
|
|
721
895
|
// Prevent unhandled promise rejection if close() rejects this request
|
|
@@ -726,15 +900,14 @@ class ImapFlow extends EventEmitter {
|
|
|
726
900
|
return promise;
|
|
727
901
|
}
|
|
728
902
|
|
|
729
|
-
// Resolves
|
|
730
|
-
// come in two forms:
|
|
903
|
+
// Resolves an untagged server response to the keyword it is dispatched on. IMAP untagged
|
|
904
|
+
// responses come in two forms:
|
|
731
905
|
// * CAPABILITY ... (keyword as command)
|
|
732
906
|
// * 42 FETCH (...) (numeric prefix + keyword)
|
|
733
|
-
// For numeric-prefixed responses
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
|
|
737
|
-
getUntaggedHandler(command, attributes) {
|
|
907
|
+
// For numeric-prefixed responses the keyword sits in the first attribute, because `command`
|
|
908
|
+
// holds the sequence number. Also used for logging, so a failure reports FETCH rather than
|
|
909
|
+
// the message number that happened to precede it.
|
|
910
|
+
normalizeUntaggedCommand(command, attributes) {
|
|
738
911
|
if (/^[0-9]+$/.test(command)) {
|
|
739
912
|
let type = attributes && attributes.length && typeof attributes[0].value === 'string' ? attributes[0].value.toUpperCase() : false;
|
|
740
913
|
if (type) {
|
|
@@ -742,7 +915,13 @@ class ImapFlow extends EventEmitter {
|
|
|
742
915
|
}
|
|
743
916
|
}
|
|
744
917
|
|
|
745
|
-
|
|
918
|
+
return command.toUpperCase().trim();
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Handler priority: command-specific handlers (registered per exec() call) take
|
|
922
|
+
// precedence over global handlers (registered on the connection).
|
|
923
|
+
getUntaggedHandler(command, attributes) {
|
|
924
|
+
command = this.normalizeUntaggedCommand(command, attributes);
|
|
746
925
|
// Check command-specific handler first (registered in exec() options.untagged)
|
|
747
926
|
if (this.currentRequest && this.currentRequest.options && this.currentRequest.options.untagged && this.currentRequest.options.untagged[command]) {
|
|
748
927
|
return this.currentRequest.options.untagged[command];
|
|
@@ -920,7 +1099,7 @@ class ImapFlow extends EventEmitter {
|
|
|
920
1099
|
err.parserError = parserError;
|
|
921
1100
|
this.rejectCurrentRequest(err);
|
|
922
1101
|
|
|
923
|
-
this.trySend().catch(sendErr => this
|
|
1102
|
+
this.trySend().catch(sendErr => logConnectionError(this, 'Failed to dispatch command', sendErr));
|
|
924
1103
|
}
|
|
925
1104
|
|
|
926
1105
|
/**
|
|
@@ -991,7 +1170,9 @@ class ImapFlow extends EventEmitter {
|
|
|
991
1170
|
try {
|
|
992
1171
|
await this.currentRequest.options.onPlusTag(parsed);
|
|
993
1172
|
} catch (err) {
|
|
994
|
-
|
|
1173
|
+
// The handler ran across an await and may have closed the connection, which
|
|
1174
|
+
// clears currentRequest, so the command name is read defensively
|
|
1175
|
+
this.log.warn({ msg: 'Failed to process continuation response', command: this.currentRequest?.command, err, cid: this.id });
|
|
995
1176
|
}
|
|
996
1177
|
return true;
|
|
997
1178
|
}
|
|
@@ -1005,7 +1186,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1005
1186
|
this.write(content);
|
|
1006
1187
|
this.log.debug({ src: 'c', msg: `(* ${content.length}B continuation *)`, cid: this.id });
|
|
1007
1188
|
} catch (err) {
|
|
1008
|
-
this
|
|
1189
|
+
logConnectionError(this, 'Failed to send literal continuation', err);
|
|
1009
1190
|
}
|
|
1010
1191
|
return true;
|
|
1011
1192
|
}
|
|
@@ -1014,12 +1195,13 @@ class ImapFlow extends EventEmitter {
|
|
|
1014
1195
|
// section[0] can be a parsed NIL (null), e.g. from a "[NIL]" response code - the
|
|
1015
1196
|
// dereference must be guarded or one such line tears down the whole connection
|
|
1016
1197
|
if (section && section.length && section[0] && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
|
1017
|
-
let
|
|
1198
|
+
let sectionKey = section[0].value.toUpperCase().trim();
|
|
1199
|
+
let sectionHandler = this.getSectionHandler(sectionKey);
|
|
1018
1200
|
if (sectionHandler) {
|
|
1019
1201
|
try {
|
|
1020
1202
|
await sectionHandler(section.slice(1));
|
|
1021
1203
|
} catch (err) {
|
|
1022
|
-
this.log.warn({ err, cid: this.id });
|
|
1204
|
+
this.log.warn({ msg: 'Failed to process response section', section: sectionKey, err, cid: this.id });
|
|
1023
1205
|
}
|
|
1024
1206
|
}
|
|
1025
1207
|
}
|
|
@@ -1030,7 +1212,14 @@ class ImapFlow extends EventEmitter {
|
|
|
1030
1212
|
try {
|
|
1031
1213
|
await untaggedHandler(parsed);
|
|
1032
1214
|
} catch (err) {
|
|
1033
|
-
|
|
1215
|
+
// Normalized only here: this runs for every untagged response, including
|
|
1216
|
+
// every FETCH, and the keyword is needed only to describe a failure
|
|
1217
|
+
this.log.warn({
|
|
1218
|
+
msg: 'Failed to process untagged response',
|
|
1219
|
+
command: this.normalizeUntaggedCommand(parsed.command, parsed.attributes),
|
|
1220
|
+
err,
|
|
1221
|
+
cid: this.id
|
|
1222
|
+
});
|
|
1034
1223
|
return true;
|
|
1035
1224
|
}
|
|
1036
1225
|
}
|
|
@@ -1141,6 +1330,9 @@ class ImapFlow extends EventEmitter {
|
|
|
1141
1330
|
|
|
1142
1331
|
if (err.responseStatus === 'NO' && txt.includes('Some of the requested messages no longer exist')) {
|
|
1143
1332
|
// Treat as successful response
|
|
1333
|
+
// Kept at warn: the caller is handed fewer messages than it asked for and
|
|
1334
|
+
// is told nothing else about it, so this entry is the only record that
|
|
1335
|
+
// the response was truncated.
|
|
1144
1336
|
this.log.warn({ msg: 'Partial FETCH response', cid: this.id, err });
|
|
1145
1337
|
await new Promise(resolve => request.resolve({ response: parsed, next: resolve }));
|
|
1146
1338
|
break;
|
|
@@ -1252,10 +1444,22 @@ class ImapFlow extends EventEmitter {
|
|
|
1252
1444
|
/**
|
|
1253
1445
|
* Socket timeout event handler.
|
|
1254
1446
|
*
|
|
1255
|
-
*
|
|
1256
|
-
*
|
|
1447
|
+
* A quiet socket is only a dead connection when something was supposed to be talking. An
|
|
1448
|
+
* idling session, a download whose consumer stopped draining, and a held mailbox lock
|
|
1449
|
+
* whose owner is busy between commands are all expected to go quiet, so the handler keeps
|
|
1450
|
+
* such a connection alive with a NOOP instead of tearing it down. An in-flight command is
|
|
1451
|
+
* the opposite: its reply is overdue, a recovery NOOP would only queue up behind it and
|
|
1452
|
+
* never reach the wire, so the timeout is reported as an error. The IDLE command itself is
|
|
1453
|
+
* the one exception - it stays in flight for as long as idling lasts, and run() breaks it
|
|
1454
|
+
* through preCheck() before the NOOP is dispatched.
|
|
1257
1455
|
*
|
|
1258
|
-
*
|
|
1456
|
+
* IDLE is not restarted here: run() re-arms auto-IDLE once the NOOP settles, and
|
|
1457
|
+
* autoidle() knows whether the connection is actually free for IDLE - an open download or
|
|
1458
|
+
* a held lock keeps just the keepalive, and with disableAutoIdle nothing restarts at all.
|
|
1459
|
+
* If the server is dead the NOOP never settles, and the next timeout fires with the NOOP
|
|
1460
|
+
* as the stuck in-flight command, which lands in the error branch below.
|
|
1461
|
+
*
|
|
1462
|
+
* @fires ImapFlow#error Emits error event if the connection cannot be recovered
|
|
1259
1463
|
*/
|
|
1260
1464
|
this._socketTimeout =
|
|
1261
1465
|
this._socketTimeout ||
|
|
@@ -1263,26 +1467,21 @@ class ImapFlow extends EventEmitter {
|
|
|
1263
1467
|
const err = new Error('Socket timeout');
|
|
1264
1468
|
err.code = 'ETIMEOUT';
|
|
1265
1469
|
|
|
1266
|
-
|
|
1470
|
+
const quietExpected = this.idling || this._openDownloads || this.currentLock;
|
|
1471
|
+
const commandStuck = this.currentRequest && !(this.idling && this.currentRequest.command === 'IDLE');
|
|
1472
|
+
|
|
1473
|
+
if (quietExpected && !commandStuck) {
|
|
1267
1474
|
if (!this.usable || !this.socket || this.socket.destroyed) {
|
|
1268
1475
|
this.emitError(err);
|
|
1269
1476
|
return;
|
|
1270
1477
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
.then(() => this.idle())
|
|
1278
|
-
.catch(err => {
|
|
1279
|
-
this.log.warn({ msg: 'IDLE recovery failed after timeout', err, cid: this.id });
|
|
1280
|
-
if (!this.isClosed) {
|
|
1281
|
-
this.close();
|
|
1282
|
-
}
|
|
1283
|
-
});
|
|
1478
|
+
this.run('NOOP').catch(err => {
|
|
1479
|
+
this.log.warn({ msg: 'Connection recovery failed after timeout', err, cid: this.id });
|
|
1480
|
+
if (!this.isClosed) {
|
|
1481
|
+
this.close();
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1284
1484
|
} else {
|
|
1285
|
-
// Close immediately for non-IDLE operations
|
|
1286
1485
|
this.log.debug({ msg: 'Socket timeout', cid: this.id });
|
|
1287
1486
|
this.emitError(err);
|
|
1288
1487
|
}
|
|
@@ -1445,7 +1644,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1445
1644
|
}
|
|
1446
1645
|
this.writeSocket.end();
|
|
1447
1646
|
} catch (err) {
|
|
1448
|
-
this.log.error({ err,
|
|
1647
|
+
this.log.error({ err, msg: 'Failed to destroy PassThrough socket', cid: this.id });
|
|
1449
1648
|
throw err;
|
|
1450
1649
|
}
|
|
1451
1650
|
};
|
|
@@ -1951,6 +2150,22 @@ class ImapFlow extends EventEmitter {
|
|
|
1951
2150
|
});
|
|
1952
2151
|
}
|
|
1953
2152
|
|
|
2153
|
+
// Reports one expunged message, either through the caller's expungeHandler or as an
|
|
2154
|
+
// 'expunge' event. Shared by the EXPUNGE and VANISHED paths so the two cannot drift.
|
|
2155
|
+
async notifyExpunge(payload) {
|
|
2156
|
+
if (typeof this.options.expungeHandler !== 'function') {
|
|
2157
|
+
this.emit('expunge', payload);
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
try {
|
|
2162
|
+
await this.options.expungeHandler(payload);
|
|
2163
|
+
} catch (err) {
|
|
2164
|
+
// The throw comes from the caller's own handler, not from this library
|
|
2165
|
+
this.log.error({ msg: 'Failed to notify expunge event', payload, err, cid: this.id });
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
1954
2169
|
async untaggedExpunge(untagged) {
|
|
1955
2170
|
if (!this.mailbox) {
|
|
1956
2171
|
// mailbox closed, ignore
|
|
@@ -1971,15 +2186,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1971
2186
|
vanished: false
|
|
1972
2187
|
};
|
|
1973
2188
|
|
|
1974
|
-
|
|
1975
|
-
try {
|
|
1976
|
-
await this.options.expungeHandler(payload);
|
|
1977
|
-
} catch (err) {
|
|
1978
|
-
this.log.error({ msg: 'Failed to notify expunge event', payload, error: err, cid: this.id });
|
|
1979
|
-
}
|
|
1980
|
-
} else {
|
|
1981
|
-
this.emit('expunge', payload);
|
|
1982
|
-
}
|
|
2189
|
+
await this.notifyExpunge(payload);
|
|
1983
2190
|
}
|
|
1984
2191
|
}
|
|
1985
2192
|
|
|
@@ -2018,15 +2225,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2018
2225
|
earlier: tags.includes('EARLIER')
|
|
2019
2226
|
};
|
|
2020
2227
|
|
|
2021
|
-
|
|
2022
|
-
try {
|
|
2023
|
-
await this.options.expungeHandler(payload);
|
|
2024
|
-
} catch (err) {
|
|
2025
|
-
this.log.error({ msg: 'Failed to notify expunge event', payload, error: err, cid: this.id });
|
|
2026
|
-
}
|
|
2027
|
-
} else {
|
|
2028
|
-
this.emit('expunge', payload);
|
|
2029
|
-
}
|
|
2228
|
+
await this.notifyExpunge(payload);
|
|
2030
2229
|
}
|
|
2031
2230
|
}
|
|
2032
2231
|
|
|
@@ -2120,6 +2319,16 @@ class ImapFlow extends EventEmitter {
|
|
|
2120
2319
|
return range;
|
|
2121
2320
|
}
|
|
2122
2321
|
|
|
2322
|
+
// The single definition of "the connection is not free". A held or queued mailbox lock, a
|
|
2323
|
+
// command in flight or queued, and an open download stream all mean a caller is
|
|
2324
|
+
// mid-sequence: starting IDLE there injects an IDLE/DONE round trip - or, with
|
|
2325
|
+
// `missingIdleCommand` set to SELECT or STATUS, a mailbox poll - between two of that
|
|
2326
|
+
// caller's own commands. Every one of those states ends by calling autoidle() again, so
|
|
2327
|
+
// declining while busy postpones IDLE, it never cancels it.
|
|
2328
|
+
connectionBusy() {
|
|
2329
|
+
return !!(this.currentLock || this.locks.length || this.currentRequest || this.requestQueue.length || this._openDownloads);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2123
2332
|
// Timer process-liveness policy: connection establishment and greeting deadlines keep the
|
|
2124
2333
|
// process alive, because a caller is waiting on connect() to settle. Background timers
|
|
2125
2334
|
// (auto-IDLE, IDLE restart, fallback polling, throttle back-off, the held-lock diagnostic) are
|
|
@@ -2130,9 +2339,22 @@ class ImapFlow extends EventEmitter {
|
|
|
2130
2339
|
if (this.options.disableAutoIdle || this.state !== this.states.SELECTED) {
|
|
2131
2340
|
return;
|
|
2132
2341
|
}
|
|
2342
|
+
|
|
2343
|
+
if (this.connectionBusy()) {
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2133
2347
|
this.idleStartTimer = setTimeout(() => {
|
|
2134
|
-
|
|
2135
|
-
|
|
2348
|
+
// Re-checked at fire time: paths that take ownership of the connection clear this
|
|
2349
|
+
// timer, but the guard must not depend on every one of them doing so - a single
|
|
2350
|
+
// missed clearTimeout would inject IDLE between a caller's own commands. Declining
|
|
2351
|
+
// postpones rather than cancels: whatever made the connection busy calls autoidle()
|
|
2352
|
+
// again when it finishes.
|
|
2353
|
+
if (this.state !== this.states.SELECTED || this.connectionBusy()) {
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
this.idle().catch(err => logConnectionError(this, 'Auto-IDLE failed', err));
|
|
2357
|
+
}, this.autoIdleDelay);
|
|
2136
2358
|
unrefTimer(this.idleStartTimer);
|
|
2137
2359
|
}
|
|
2138
2360
|
|
|
@@ -2192,16 +2414,21 @@ class ImapFlow extends EventEmitter {
|
|
|
2192
2414
|
throw new Error('Failed to setup proxy connection');
|
|
2193
2415
|
}
|
|
2194
2416
|
} catch (err) {
|
|
2417
|
+
// Logged here rather than relying on proxy-connection.js, which only reports
|
|
2418
|
+
// failures from inside the two connect helpers. An unsupported scheme, a proxy URL
|
|
2419
|
+
// that will not parse and a deadline that expired before the connect started all
|
|
2420
|
+
// reject before any logging happens there, so this is the one place that sees
|
|
2421
|
+
// every way proxy setup can fail.
|
|
2422
|
+
this.log.error({ msg: 'Failed to setup proxy connection', err, cid: this.id });
|
|
2423
|
+
|
|
2195
2424
|
if (err.code === 'CONNECT_TIMEOUT') {
|
|
2196
2425
|
// The shared deadline expired during proxy setup. Report it as the documented
|
|
2197
2426
|
// connection timeout rather than as a generic proxy failure.
|
|
2198
|
-
this.log.error({ err, cid: this.id });
|
|
2199
2427
|
throw err;
|
|
2200
2428
|
}
|
|
2201
2429
|
let error = new Error('Failed to setup proxy connection');
|
|
2202
2430
|
error.code = err.code || 'ProxyError';
|
|
2203
2431
|
error._err = err;
|
|
2204
|
-
this.log.error({ error, cid: this.id });
|
|
2205
2432
|
throw error;
|
|
2206
2433
|
}
|
|
2207
2434
|
}
|
|
@@ -2410,7 +2637,9 @@ class ImapFlow extends EventEmitter {
|
|
|
2410
2637
|
}
|
|
2411
2638
|
|
|
2412
2639
|
if (typeof this.preCheck === 'function') {
|
|
2413
|
-
|
|
2640
|
+
// Runs while the connection is being torn down, so the rejection this sees is
|
|
2641
|
+
// almost always the NoConnection close() is about to raise itself.
|
|
2642
|
+
this.preCheck().catch(err => logConnectionError(this, 'Failed to break IDLE while closing', err));
|
|
2414
2643
|
}
|
|
2415
2644
|
|
|
2416
2645
|
// Session-only public state must not survive the connection it describes: callers read
|
|
@@ -2503,7 +2732,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2503
2732
|
this._inflate.destroy();
|
|
2504
2733
|
this._inflate = null;
|
|
2505
2734
|
} catch (err) {
|
|
2506
|
-
this.log.error({ err,
|
|
2735
|
+
this.log.error({ err, msg: 'Failed to destroy inflate stream', cid: this.id });
|
|
2507
2736
|
}
|
|
2508
2737
|
}
|
|
2509
2738
|
|
|
@@ -2513,7 +2742,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2513
2742
|
this._deflate.destroy();
|
|
2514
2743
|
this._deflate = null;
|
|
2515
2744
|
} catch (err) {
|
|
2516
|
-
this.log.error({ err,
|
|
2745
|
+
this.log.error({ err, msg: 'Failed to destroy deflate stream', cid: this.id });
|
|
2517
2746
|
}
|
|
2518
2747
|
}
|
|
2519
2748
|
|
|
@@ -2531,7 +2760,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2531
2760
|
this.streamer.destroy();
|
|
2532
2761
|
}
|
|
2533
2762
|
} catch (err) {
|
|
2534
|
-
this.log.error({ err,
|
|
2763
|
+
this.log.error({ err, msg: 'Failed to cleanup streamer', cid: this.id });
|
|
2535
2764
|
}
|
|
2536
2765
|
}
|
|
2537
2766
|
|
|
@@ -2584,7 +2813,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2584
2813
|
this._socketEnd = null;
|
|
2585
2814
|
this._socketTimeout = null;
|
|
2586
2815
|
|
|
2587
|
-
this.log.
|
|
2816
|
+
this.log.debug({
|
|
2588
2817
|
msg: 'Connection closed',
|
|
2589
2818
|
cid: this.id,
|
|
2590
2819
|
...(this._unknownTagCount ? { unknownTagCount: this._unknownTagCount } : {})
|
|
@@ -2600,7 +2829,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2600
2829
|
this.emit('close');
|
|
2601
2830
|
} catch (ex) {
|
|
2602
2831
|
// close failed
|
|
2603
|
-
this.log.error(ex);
|
|
2832
|
+
this.log.error({ err: ex, cid: this.id });
|
|
2604
2833
|
}
|
|
2605
2834
|
}
|
|
2606
2835
|
|
|
@@ -3963,6 +4192,23 @@ class ImapFlow extends EventEmitter {
|
|
|
3963
4192
|
}
|
|
3964
4193
|
};
|
|
3965
4194
|
|
|
4195
|
+
// A download is a sequence of chunk FETCHes with a backpressure wait in between. Those
|
|
4196
|
+
// gaps look exactly like an inactive connection, so without this auto-IDLE would start
|
|
4197
|
+
// between chunks and the next chunk would have to break it again - two extra round
|
|
4198
|
+
// trips per chunk, for as long as the consumer is slow. Counted before control returns
|
|
4199
|
+
// to the event loop: the head chunk's own FETCH already armed the auto-IDLE timer, and
|
|
4200
|
+
// with a very short autoIdleDelay that timer could otherwise fire before the deferred
|
|
4201
|
+
// chunk loop below has marked the download open.
|
|
4202
|
+
this._openDownloads++;
|
|
4203
|
+
let downloadDone = false;
|
|
4204
|
+
let finishDownload = () => {
|
|
4205
|
+
if (!downloadDone) {
|
|
4206
|
+
downloadDone = true;
|
|
4207
|
+
this._openDownloads--;
|
|
4208
|
+
this.autoidle();
|
|
4209
|
+
}
|
|
4210
|
+
};
|
|
4211
|
+
|
|
3966
4212
|
// Kick off the download pipeline asynchronously. The first chunk was
|
|
3967
4213
|
// already fetched above (to get metadata); write it to the decoder
|
|
3968
4214
|
// stream and then fetch remaining chunks via fetchAllParts().
|
|
@@ -3987,6 +4233,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3987
4233
|
/* c8 ignore stop */
|
|
3988
4234
|
})
|
|
3989
4235
|
.finally(() => {
|
|
4236
|
+
finishDownload();
|
|
3990
4237
|
if (!fetchAborted && stream && !stream.destroyed) {
|
|
3991
4238
|
stream.end();
|
|
3992
4239
|
}
|
|
@@ -3999,6 +4246,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3999
4246
|
writeResult = writeChunk(chunk);
|
|
4000
4247
|
} catch (err) {
|
|
4001
4248
|
stream.emit('error', err);
|
|
4249
|
+
finishDownload();
|
|
4002
4250
|
/* c8 ignore next 3 */ // emitting the error above triggers cleanup (fetchAborted=true), so this end() guard is already false here
|
|
4003
4251
|
if (!fetchAborted && stream && !stream.destroyed) {
|
|
4004
4252
|
stream.end();
|
|
@@ -4006,12 +4254,14 @@ class ImapFlow extends EventEmitter {
|
|
|
4006
4254
|
return;
|
|
4007
4255
|
}
|
|
4008
4256
|
|
|
4009
|
-
/* c8 ignore next
|
|
4257
|
+
/* c8 ignore next 9 */ // `stream` is piped to the limiter before this runs, so the head write drains synchronously and always returns true (verified for chunkSize up to 8MB); the drain-wait branch is unreachable
|
|
4010
4258
|
if (!writeResult) {
|
|
4011
4259
|
// Initial chunk filled the buffer, wait for drain
|
|
4012
4260
|
stream.once('drain', () => {
|
|
4013
4261
|
if (!fetchAborted) {
|
|
4014
4262
|
runFetchAllParts();
|
|
4263
|
+
} else {
|
|
4264
|
+
finishDownload();
|
|
4015
4265
|
}
|
|
4016
4266
|
});
|
|
4017
4267
|
} else {
|
|
@@ -4182,18 +4432,22 @@ class ImapFlow extends EventEmitter {
|
|
|
4182
4432
|
|
|
4183
4433
|
clearTimeout(this.idleStartTimer);
|
|
4184
4434
|
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4435
|
+
try {
|
|
4436
|
+
// The preCheck (breaking an active IDLE) sits inside the try on purpose: the
|
|
4437
|
+
// clearTimeout above is unconditional, so every exit - a failed command or a
|
|
4438
|
+
// preCheck that rejects - must still reach the finally, or auto-IDLE would stay
|
|
4439
|
+
// disarmed on an otherwise healthy connection until some later command succeeded.
|
|
4440
|
+
if (typeof this.preCheck === 'function') {
|
|
4441
|
+
await this.preCheck();
|
|
4442
|
+
}
|
|
4190
4443
|
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4444
|
+
return await this.runInternal(command, ...args);
|
|
4445
|
+
} finally {
|
|
4446
|
+
if (command !== 'IDLE') {
|
|
4447
|
+
// do not autostart IDLE, if IDLE itself was stopped
|
|
4448
|
+
this.autoidle();
|
|
4449
|
+
}
|
|
4194
4450
|
}
|
|
4195
|
-
|
|
4196
|
-
return result;
|
|
4197
4451
|
}
|
|
4198
4452
|
|
|
4199
4453
|
/**
|
|
@@ -4314,6 +4568,10 @@ class ImapFlow extends EventEmitter {
|
|
|
4314
4568
|
idling: this.idling
|
|
4315
4569
|
});
|
|
4316
4570
|
this.currentLock = false;
|
|
4571
|
+
// autoidle() will not arm while a lock is held, so the release is what
|
|
4572
|
+
// restarts it. It re-checks the queue itself, so a lock waiting behind
|
|
4573
|
+
// this one still keeps IDLE off.
|
|
4574
|
+
this.autoidle();
|
|
4317
4575
|
// Use setImmediate to avoid stack overflow
|
|
4318
4576
|
setImmediate(() => {
|
|
4319
4577
|
this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
|
|
@@ -4335,6 +4593,18 @@ class ImapFlow extends EventEmitter {
|
|
|
4335
4593
|
continue; // Process next lock in queue
|
|
4336
4594
|
}
|
|
4337
4595
|
|
|
4596
|
+
// Both grant paths finish the same way. autoidle() is re-checked because a stale
|
|
4597
|
+
// auto-IDLE timer may still be armed at this point: on the SELECT path run()
|
|
4598
|
+
// re-arms auto-IDLE when the SELECT settles - a moment before currentLock is set -
|
|
4599
|
+
// and the fast path can inherit a timer from an earlier command. Either way the
|
|
4600
|
+
// timer must not fire inside the lock.
|
|
4601
|
+
const grantLock = () => {
|
|
4602
|
+
this.currentLock = lock;
|
|
4603
|
+
armHeldTimer();
|
|
4604
|
+
this.autoidle();
|
|
4605
|
+
resolve({ path, release });
|
|
4606
|
+
};
|
|
4607
|
+
|
|
4338
4608
|
if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
|
|
4339
4609
|
// Fast path: mailbox is already selected with the right access mode
|
|
4340
4610
|
this.log.trace({
|
|
@@ -4344,9 +4614,7 @@ class ImapFlow extends EventEmitter {
|
|
|
4344
4614
|
idling: this.idling,
|
|
4345
4615
|
...(options.description && { description: options.description })
|
|
4346
4616
|
});
|
|
4347
|
-
|
|
4348
|
-
armHeldTimer();
|
|
4349
|
-
resolve({ path, release });
|
|
4617
|
+
grantLock();
|
|
4350
4618
|
break; // Stop processing; next lock waits for release()
|
|
4351
4619
|
}
|
|
4352
4620
|
|
|
@@ -4360,9 +4628,7 @@ class ImapFlow extends EventEmitter {
|
|
|
4360
4628
|
idling: this.idling,
|
|
4361
4629
|
...(options.description && { description: options.description })
|
|
4362
4630
|
});
|
|
4363
|
-
|
|
4364
|
-
armHeldTimer();
|
|
4365
|
-
resolve({ path, release });
|
|
4631
|
+
grantLock();
|
|
4366
4632
|
break; // Wait for this lock to be released
|
|
4367
4633
|
} catch (err) {
|
|
4368
4634
|
if (err.responseStatus === 'NO') {
|
|
@@ -4497,7 +4763,19 @@ class ImapFlow extends EventEmitter {
|
|
|
4497
4763
|
// we are checking to make sure the level is supported.
|
|
4498
4764
|
// if it isn't supported but the level is error or fatal, log to console anyway.
|
|
4499
4765
|
if (level === 'fatal' || level === 'error') {
|
|
4500
|
-
|
|
4766
|
+
let entry = args[0];
|
|
4767
|
+
try {
|
|
4768
|
+
if (entry && typeof entry === 'object' && entry.err) {
|
|
4769
|
+
entry = Object.assign({}, entry, { err: flattenLoggedError(entry.err) });
|
|
4770
|
+
}
|
|
4771
|
+
console.error(JSON.stringify(entry));
|
|
4772
|
+
} catch {
|
|
4773
|
+
// Serializing failed (a circular structure, a BigInt, a throwing
|
|
4774
|
+
// getter). This fallback exists so an error is never lost, so hand
|
|
4775
|
+
// the entry to console.error itself - it inspects rather than
|
|
4776
|
+
// serializes, and handles all three - instead of dropping it.
|
|
4777
|
+
console.error(entry);
|
|
4778
|
+
}
|
|
4501
4779
|
}
|
|
4502
4780
|
} else {
|
|
4503
4781
|
mainLogger[level](...args);
|
|
@@ -4505,18 +4783,21 @@ class ImapFlow extends EventEmitter {
|
|
|
4505
4783
|
}
|
|
4506
4784
|
|
|
4507
4785
|
if (this.emitLogs && args && args[0] && typeof args[0] === 'object') {
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
logEntry.err
|
|
4517
|
-
}
|
|
4786
|
+
// Guarded for the same reason as the console fallback above: a log call must
|
|
4787
|
+
// never throw. Most of these run inside catch blocks in the protocol
|
|
4788
|
+
// machinery, where a throw would escape the handler that was recovering from
|
|
4789
|
+
// something else and strand the connection. A throwing property getter on the
|
|
4790
|
+
// logged error and a throwing 'log' listener both end up here.
|
|
4791
|
+
try {
|
|
4792
|
+
let logEntry = Object.assign({ level, t: Date.now(), cid: this.id, lo: ++this.lo }, args[0]);
|
|
4793
|
+
if (logEntry.err) {
|
|
4794
|
+
logEntry.err = flattenLoggedError(logEntry.err);
|
|
4795
|
+
}
|
|
4796
|
+
this.emit('log', logEntry);
|
|
4797
|
+
} catch {
|
|
4798
|
+
// Nothing to do with it: reporting the failure would re-enter this
|
|
4799
|
+
// same path
|
|
4518
4800
|
}
|
|
4519
|
-
this.emit('log', logEntry);
|
|
4520
4801
|
}
|
|
4521
4802
|
};
|
|
4522
4803
|
}
|