imapflow 2.3.0 → 2.4.0
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 +12 -0
- package/dist/cjs/commands/append.d.ts +1 -1
- package/dist/cjs/commands/append.js +15 -4
- package/dist/cjs/commands/compress.js +2 -2
- package/dist/cjs/commands/idle.js +2 -3
- package/dist/cjs/commands/status.js +2 -4
- package/dist/cjs/connection-deadline.d.ts +1 -1
- package/dist/cjs/connection-deadline.js +2 -4
- package/dist/cjs/download.js +22 -19
- package/dist/cjs/errors.d.ts +15 -0
- package/dist/cjs/errors.js +22 -0
- package/dist/cjs/handler/imap-compiler.d.ts +4 -4
- package/dist/cjs/handler/imap-compiler.js +18 -18
- package/dist/cjs/handler/imap-parser.js +28 -3
- package/dist/cjs/handler/imap-stream.js +9 -8
- package/dist/cjs/handler/limits.d.ts +1 -1
- package/dist/cjs/handler/limits.js +5 -7
- package/dist/cjs/handler/parser-instance.js +23 -38
- package/dist/cjs/handler/token-parser.d.ts +12 -0
- package/dist/cjs/handler/token-parser.js +44 -104
- package/dist/cjs/imap-flow.d.ts +4 -3
- package/dist/cjs/imap-flow.js +72 -72
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/proxy-connection.js +2 -5
- package/dist/cjs/search-compiler.js +2 -3
- package/dist/cjs/tools.d.ts +2 -2
- package/dist/cjs/tools.js +8 -11
- package/dist/esm/commands/append.d.ts +1 -1
- package/dist/esm/commands/append.js +15 -4
- package/dist/esm/commands/compress.js +2 -2
- package/dist/esm/commands/idle.js +2 -3
- package/dist/esm/commands/status.js +2 -4
- package/dist/esm/connection-deadline.d.ts +1 -1
- package/dist/esm/connection-deadline.js +2 -4
- package/dist/esm/download.js +22 -19
- package/dist/esm/errors.d.ts +15 -0
- package/dist/esm/errors.js +21 -0
- package/dist/esm/handler/imap-compiler.d.ts +4 -4
- package/dist/esm/handler/imap-compiler.js +18 -18
- package/dist/esm/handler/imap-parser.js +28 -3
- package/dist/esm/handler/imap-stream.js +9 -8
- package/dist/esm/handler/limits.d.ts +1 -1
- package/dist/esm/handler/limits.js +5 -7
- package/dist/esm/handler/parser-instance.js +23 -38
- package/dist/esm/handler/token-parser.d.ts +12 -0
- package/dist/esm/handler/token-parser.js +44 -104
- package/dist/esm/imap-flow.d.ts +4 -3
- package/dist/esm/imap-flow.js +73 -73
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/proxy-connection.js +2 -5
- package/dist/esm/search-compiler.js +2 -3
- package/dist/esm/tools.d.ts +2 -2
- package/dist/esm/tools.js +9 -12
- package/package.json +3 -3
package/dist/cjs/imap-flow.js
CHANGED
|
@@ -85,6 +85,10 @@ const RAW_SENSITIVE_COMMANDS = new Set(['LOGIN', 'AUTHENTICATE']);
|
|
|
85
85
|
// Stand-in payload for a withheld raw client frame. Fixed width, so the entry says nothing
|
|
86
86
|
// about the length of what it replaced.
|
|
87
87
|
const RAW_HIDDEN_PLACEHOLDER = Buffer.from('(* value hidden *)\r\n').toString('base64');
|
|
88
|
+
// From this size up, a Buffer written as the last part of a command gets its CRLF as a socket
|
|
89
|
+
// write of its own instead of being copied to append it (see write())
|
|
90
|
+
const SEPARATE_LINE_BREAK_SIZE = 64 * 1024;
|
|
91
|
+
const CRLF = Buffer.from('\r\n');
|
|
88
92
|
// Servers, matched by the name in their ID response, that advertise IMAP4rev2 next to
|
|
89
93
|
// IMAP4rev1 and accept ENABLE IMAP4rev2, but misbehave once it is enabled. ENABLE cannot
|
|
90
94
|
// be undone (RFC 5161), so these are kept in IMAP4rev1 mode from the start. Strato's
|
|
@@ -385,43 +389,47 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
385
389
|
// commandParts holds the remaining parts (literal data, continuation); the CRLF
|
|
386
390
|
// delimiter is only added when no more parts remain (the command is complete).
|
|
387
391
|
let addLineBreak = !this.commandParts.length;
|
|
388
|
-
|
|
392
|
+
// What goes to the socket, one write per entry. A large Buffer (the data of a literal)
|
|
393
|
+
// gets its CRLF as a write of its own, as concatenating would copy all of it.
|
|
394
|
+
let frames;
|
|
389
395
|
if (typeof chunk === 'string') {
|
|
390
396
|
if (addLineBreak) {
|
|
391
397
|
chunk += '\r\n';
|
|
392
398
|
}
|
|
393
|
-
|
|
399
|
+
frames = [Buffer.from(chunk, 'binary')];
|
|
394
400
|
}
|
|
395
401
|
else if (Buffer.isBuffer(chunk)) {
|
|
396
|
-
if (addLineBreak) {
|
|
397
|
-
|
|
402
|
+
if (!addLineBreak) {
|
|
403
|
+
frames = [chunk];
|
|
398
404
|
}
|
|
399
405
|
else {
|
|
400
|
-
|
|
406
|
+
frames = chunk.length >= SEPARATE_LINE_BREAK_SIZE ? [chunk, CRLF] : [Buffer.concat([chunk, CRLF])];
|
|
401
407
|
}
|
|
402
408
|
}
|
|
403
409
|
else {
|
|
404
410
|
return false;
|
|
405
411
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
412
|
+
for (let data of frames) {
|
|
413
|
+
if (this.logRaw) {
|
|
414
|
+
// Client frames of an authentication exchange carry credentials: the LOGIN
|
|
415
|
+
// arguments, and for AUTHENTICATE also the continuation writes (SASL PLAIN
|
|
416
|
+
// response, AUTH=LOGIN password, OAuth token payload) that bypass send(). The
|
|
417
|
+
// parsed command log masks these, so the raw log must withhold them too, but
|
|
418
|
+
// `data` still carries the placeholder rather than being dropped - the field is
|
|
419
|
+
// part of the documented log format and consumers decode it unconditionally.
|
|
420
|
+
this.log.trace({
|
|
421
|
+
src: 'c',
|
|
422
|
+
msg: 'write to socket',
|
|
423
|
+
data: this.rawSensitiveCommand ? RAW_HIDDEN_PLACEHOLDER : data.toString('base64'),
|
|
424
|
+
...(this.rawSensitiveCommand ? { hidden: true } : {}),
|
|
425
|
+
compress: !!this._deflate,
|
|
426
|
+
secure: !!this.secureConnection,
|
|
427
|
+
cid: this.id
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
this.writeBytesCounter += data.length;
|
|
431
|
+
this.writeSocket.write(data);
|
|
422
432
|
}
|
|
423
|
-
this.writeBytesCounter += data.length;
|
|
424
|
-
this.writeSocket.write(data);
|
|
425
433
|
}
|
|
426
434
|
/**
|
|
427
435
|
* Returns byte counters for the current connection.
|
|
@@ -447,6 +455,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
447
455
|
// and once as a string (for logging, with sensitive data masked).
|
|
448
456
|
// When LITERAL- or LITERAL+ extensions are available, the compiler can use
|
|
449
457
|
// non-synchronizing literals to avoid waiting for server "+" continuation.
|
|
458
|
+
// Async although nothing in it awaits: a failure inside it rejects one microtask later, so a
|
|
459
|
+
// close() in the same tick settles the command with NoConnection first (see
|
|
460
|
+
// test/unhandled-rejection-test.ts)
|
|
450
461
|
/** @internal */
|
|
451
462
|
async send(data) {
|
|
452
463
|
if (this.state === this.states.LOGOUT) {
|
|
@@ -456,11 +467,11 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
456
467
|
// the socket watchdog.
|
|
457
468
|
throw this.createNoConnectionError(false, { rejectedFrom: 'sendAfterLogout', command: data.command });
|
|
458
469
|
}
|
|
459
|
-
// Classify
|
|
460
|
-
//
|
|
470
|
+
// Classify once per command. Every frame of this command - the command line and any
|
|
471
|
+
// continuation write that follows it - belongs to it until the next send(), because
|
|
461
472
|
// trySend() keeps one command in flight at a time. Reading currentRequest inside write()
|
|
462
|
-
// instead would be racy: rejectCurrentRequest() can clear it
|
|
463
|
-
//
|
|
473
|
+
// instead would be racy: rejectCurrentRequest() can clear it before a continuation frame
|
|
474
|
+
// is written, and the credential frame would then be logged in the clear.
|
|
464
475
|
// Uppercased because the wire protocol is case-insensitive and exec() passes the
|
|
465
476
|
// caller's spelling through unchanged. The command list covers the mechanisms whose
|
|
466
477
|
// secret arrives in a continuation frame, which carries no attributes of its own; the
|
|
@@ -471,7 +482,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
471
482
|
// Compile with asArray=true: splits output into parts for literal handling.
|
|
472
483
|
// First part is the command text up to the first literal, remaining parts
|
|
473
484
|
// are stored in this.commandParts and sent after server "+" continuations.
|
|
474
|
-
let compiled =
|
|
485
|
+
let compiled = (0, imap_handler_js_1.compiler)(data, {
|
|
475
486
|
asArray: true,
|
|
476
487
|
// LITERAL- is part of base IMAP4rev2
|
|
477
488
|
literalMinus: (0, tools_js_1.hasCapability)(this, 'LITERAL-') || this.capabilities.has('LITERAL+')
|
|
@@ -482,7 +493,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
482
493
|
// Compile again for logging with isLogging=true: masks sensitive values
|
|
483
494
|
// like passwords while producing a human-readable command string
|
|
484
495
|
if (this.isLogLevelEnabled('debug')) {
|
|
485
|
-
let logCompiled =
|
|
496
|
+
let logCompiled = (0, imap_handler_js_1.compiler)(data, {
|
|
486
497
|
isLogging: true
|
|
487
498
|
});
|
|
488
499
|
this.log.debug({ src: 'c', msg: logCompiled.toString(), cid: this.id, comment: options.comment });
|
|
@@ -739,9 +750,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
739
750
|
// parser would keep waiting on its backpressure callback forever, which is a
|
|
740
751
|
// silent permanent hang. Fail closed instead.
|
|
741
752
|
keepReading = false;
|
|
742
|
-
let error =
|
|
743
|
-
error.code = 'ResponseProcessingFailed';
|
|
744
|
-
error._err = err;
|
|
753
|
+
let error = (0, errors_js_1.createImapError)('Failed to process server response', 'ResponseProcessingFailed', { _err: err });
|
|
745
754
|
this.log.error({ msg: 'Failed to process server response', err, cid: this.id });
|
|
746
755
|
this.rejectCurrentRequest(error);
|
|
747
756
|
this.failProtocol(error);
|
|
@@ -864,7 +873,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
864
873
|
// skipped when the level is off.
|
|
865
874
|
let logLevel = /^\d+$/.test(parsed.command || '') && parsed.attributes && parsed.attributes[0] && parsed.attributes[0].value === 'FETCH' ? 'trace' : 'debug';
|
|
866
875
|
if (this.isLogLevelEnabled(logLevel)) {
|
|
867
|
-
let logCompiled =
|
|
876
|
+
let logCompiled = (0, imap_handler_js_1.compiler)(parsed, {
|
|
868
877
|
isLogging: true
|
|
869
878
|
});
|
|
870
879
|
this.log[logLevel]({ src: 's', msg: logCompiled.toString(), cid: this.id, nullBytesRemoved: parsed.nullBytesRemoved });
|
|
@@ -970,12 +979,12 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
970
979
|
// response ambiguous, so reject this request and fail the connection closed.
|
|
971
980
|
let request = this.requestTagMap.get(parsed.tag);
|
|
972
981
|
this.requestTagMap.delete(parsed.tag);
|
|
973
|
-
let err =
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
};
|
|
982
|
+
let err = (0, errors_js_1.createImapError)('Server sent a tagged response for a command that was not in flight', 'UnexpectedTag', {
|
|
983
|
+
details: {
|
|
984
|
+
received: parsed.tag,
|
|
985
|
+
expected: this.currentRequest ? this.currentRequest.tag : null
|
|
986
|
+
}
|
|
987
|
+
});
|
|
979
988
|
this.log.error({ msg: 'Protocol desynchronization', err, cid: this.id });
|
|
980
989
|
request.reject(err);
|
|
981
990
|
this.failProtocol(err);
|
|
@@ -1019,9 +1028,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1019
1028
|
try {
|
|
1020
1029
|
err.executedCommand =
|
|
1021
1030
|
parsed.tag +
|
|
1022
|
-
(
|
|
1031
|
+
(0, imap_handler_js_1.compiler)(request, {
|
|
1023
1032
|
isLogging: true
|
|
1024
|
-
})
|
|
1033
|
+
}).toString();
|
|
1025
1034
|
}
|
|
1026
1035
|
catch {
|
|
1027
1036
|
// ignore
|
|
@@ -1074,9 +1083,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1074
1083
|
break;
|
|
1075
1084
|
}
|
|
1076
1085
|
default: {
|
|
1077
|
-
let err =
|
|
1078
|
-
err.code = 'InvalidResponse';
|
|
1079
|
-
err.response = parsed;
|
|
1086
|
+
let err = (0, errors_js_1.createImapError)('Invalid server response', 'InvalidResponse', { response: parsed });
|
|
1080
1087
|
request.reject(err);
|
|
1081
1088
|
break;
|
|
1082
1089
|
}
|
|
@@ -1177,11 +1184,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1177
1184
|
this._socketTimeout =
|
|
1178
1185
|
this._socketTimeout ||
|
|
1179
1186
|
(() => {
|
|
1180
|
-
const timeoutError = () =>
|
|
1181
|
-
const err = new Error('Socket timeout');
|
|
1182
|
-
err.code = 'ETIMEOUT';
|
|
1183
|
-
return err;
|
|
1184
|
-
};
|
|
1187
|
+
const timeoutError = () => (0, errors_js_1.createImapError)('Socket timeout', 'ETIMEOUT');
|
|
1185
1188
|
if (!this.usable || !this.socket || this.socket.destroyed) {
|
|
1186
1189
|
this.emitError(timeoutError());
|
|
1187
1190
|
return;
|
|
@@ -1365,7 +1368,10 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1365
1368
|
// to call deflate.flush() after each IMAP command to push all pending
|
|
1366
1369
|
// compressed bytes to the server immediately (IMAP is request-response).
|
|
1367
1370
|
const writeSocket = new node_stream_1.PassThrough({
|
|
1368
|
-
highWaterMark: 64 * 1024 // 64KB buffer limit to prevent excessive memory usage
|
|
1371
|
+
highWaterMark: 64 * 1024, // 64KB buffer limit to prevent excessive memory usage
|
|
1372
|
+
// read() hands out each written chunk as is: in byte mode it joins whatever is
|
|
1373
|
+
// queued into a new Buffer, which copies every large literal once more
|
|
1374
|
+
readableObjectMode: true
|
|
1369
1375
|
});
|
|
1370
1376
|
this.writeSocket = writeSocket;
|
|
1371
1377
|
/* c8 ignore start */ // destroySoon override is never invoked by ImapFlow (close() calls destroy()); kept for stream API completeness
|
|
@@ -1493,9 +1499,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1493
1499
|
// socket; injection that still races in afterwards corrupts the TLS handshake and
|
|
1494
1500
|
// is rejected there instead (with a generic TLS error rather than STARTTLS_INJECTION).
|
|
1495
1501
|
const failSTARTTLSInjection = () => {
|
|
1496
|
-
let err =
|
|
1497
|
-
err.code = 'STARTTLS_INJECTION';
|
|
1498
|
-
err.tlsFailed = true;
|
|
1502
|
+
let err = (0, errors_js_1.createImapError)('Server sent data after the STARTTLS response and before the TLS handshake; possible plaintext-injection attack', 'STARTTLS_INJECTION', { tlsFailed: true });
|
|
1499
1503
|
this.closeAfter();
|
|
1500
1504
|
return err;
|
|
1501
1505
|
};
|
|
@@ -1578,8 +1582,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1578
1582
|
socketPlain.on('error', settle);
|
|
1579
1583
|
/* c8 ignore start */ // UPGRADE_TIMEOUT is 10s; firing it deterministically would make the test suite hang
|
|
1580
1584
|
this.upgradeTimeout = setTimeout(() => {
|
|
1581
|
-
let err =
|
|
1582
|
-
err.code = 'UPGRADE_TIMEOUT';
|
|
1585
|
+
let err = (0, errors_js_1.createImapError)('Failed to upgrade connection in required time', 'UPGRADE_TIMEOUT');
|
|
1583
1586
|
settle(err);
|
|
1584
1587
|
}, UPGRADE_TIMEOUT);
|
|
1585
1588
|
/* c8 ignore stop */
|
|
@@ -2083,9 +2086,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2083
2086
|
async connect() {
|
|
2084
2087
|
if (this._connectCalled) {
|
|
2085
2088
|
// Prevent re-using ImapFlow instances by allowing to call connect just once.
|
|
2086
|
-
|
|
2087
|
-
err.code = 'InstanceReused';
|
|
2088
|
-
throw err;
|
|
2089
|
+
throw (0, errors_js_1.createImapError)('Can not re-use ImapFlow instance', 'InstanceReused');
|
|
2089
2090
|
}
|
|
2090
2091
|
this._connectCalled = true;
|
|
2091
2092
|
let closedError = () => this.createNoConnectionError(this.byeReason, { rejectedFrom: 'connect' });
|
|
@@ -2173,14 +2174,14 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2173
2174
|
(0, proxy_connection_js_1.detachEarlyErrorHandler)(socket);
|
|
2174
2175
|
this.configureSocket(this.socket);
|
|
2175
2176
|
this.greetingTimeout = setTimeout(() => {
|
|
2176
|
-
let err =
|
|
2177
|
+
let err = (0, errors_js_1.createImapError)(
|
|
2177
2178
|
/* c8 ignore next */ // the greeting-timeout test uses a plaintext socket; the secure-socket branch of this hint is not separately exercised
|
|
2178
|
-
`Failed to receive greeting from server in required time${!this.secureConnection ? '. Maybe should use TLS?' : ''}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
};
|
|
2179
|
+
`Failed to receive greeting from server in required time${!this.secureConnection ? '. Maybe should use TLS?' : ''}`, 'GREETING_TIMEOUT', {
|
|
2180
|
+
details: {
|
|
2181
|
+
/* c8 ignore next */ // firing the timeout with the default (large) value would hang the suite, so only the explicit-option path is tested
|
|
2182
|
+
greetingTimeout: this.options.greetingTimeout || GREETING_TIMEOUT
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2184
2185
|
this.log.error({ err, cid: this.id });
|
|
2185
2186
|
this.closeAfter();
|
|
2186
2187
|
reject(err);
|
|
@@ -2404,9 +2405,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2404
2405
|
let reject = this.initialReject;
|
|
2405
2406
|
this.initialResolve = false;
|
|
2406
2407
|
this.initialReject = false;
|
|
2407
|
-
let err =
|
|
2408
|
+
let err = (0, errors_js_1.createImapError)('Unexpected close',
|
|
2408
2409
|
/* c8 ignore next */ // closing a pending connect over an already-secure socket (the TLS branch) is not separately exercised
|
|
2409
|
-
|
|
2410
|
+
this.secureConnection ? 'ClosedAfterConnectTLS' : 'ClosedAfterConnectText');
|
|
2410
2411
|
// Surface the server's BYE reason (e.g. "Too many connections") when the
|
|
2411
2412
|
// connection was closed by an untagged BYE, so the caller sees why.
|
|
2412
2413
|
if (this.byeReason) {
|
|
@@ -2917,7 +2918,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2917
2918
|
* Appends a new message to a mailbox
|
|
2918
2919
|
*
|
|
2919
2920
|
* @param path Mailbox path to upload the message to (unicode string). If value is an array then it is joined using current delimiter symbols. Namespace prefix is added automatically if required.
|
|
2920
|
-
* @param content RFC822 formatted email message
|
|
2921
|
+
* @param content RFC822 formatted email message, as a Buffer, Uint8Array or string
|
|
2921
2922
|
* @param flags an array of flags to be set for the uploaded message
|
|
2922
2923
|
* @param idate internal date to be set for the message
|
|
2923
2924
|
* @returns info about uploaded message
|
|
@@ -3219,7 +3220,8 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3219
3220
|
* // download body part nr '1.2' from latest message
|
|
3220
3221
|
* let download = await client.download('*', '1.2');
|
|
3221
3222
|
* if (download.content) {
|
|
3222
|
-
* download.
|
|
3223
|
+
* // pipeline() rejects when the download fails, a plain .pipe() would not report it
|
|
3224
|
+
* await stream.promises.pipeline(download.content, fs.createWriteStream(download.meta.filename));
|
|
3223
3225
|
* }
|
|
3224
3226
|
*/
|
|
3225
3227
|
async download(range, part, options) {
|
|
@@ -3556,9 +3558,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3556
3558
|
const idx = this.locks.indexOf(lockEntry);
|
|
3557
3559
|
if (idx !== -1) {
|
|
3558
3560
|
this.locks.splice(idx, 1);
|
|
3559
|
-
let err =
|
|
3560
|
-
err.code = 'LockTimeout';
|
|
3561
|
-
err.lockId = lockEntry.lockId;
|
|
3561
|
+
let err = (0, errors_js_1.createImapError)('Timed out waiting for mailbox lock', 'LockTimeout', { lockId: lockEntry.lockId });
|
|
3562
3562
|
reject(err);
|
|
3563
3563
|
}
|
|
3564
3564
|
}, Number(lockOptions.acquireTimeout));
|
package/dist/cjs/package-info.js
CHANGED
|
@@ -10,6 +10,7 @@ const node_net_1 = __importDefault(require("node:net"));
|
|
|
10
10
|
const node_tls_1 = __importDefault(require("node:tls"));
|
|
11
11
|
const connection_deadline_js_1 = require("./connection-deadline.js");
|
|
12
12
|
const tools_js_1 = require("./tools.js");
|
|
13
|
+
const errors_js_1 = require("./errors.js");
|
|
13
14
|
// Cap the CONNECT response buffered before the header terminator, so a proxy that never sends
|
|
14
15
|
// \r\n\r\n cannot grow memory without bound.
|
|
15
16
|
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
|
@@ -33,11 +34,7 @@ const redactUrl = (proxyUrl) => {
|
|
|
33
34
|
}
|
|
34
35
|
return redacted.href;
|
|
35
36
|
};
|
|
36
|
-
const proxyError = (message, code) =>
|
|
37
|
-
let err = new Error(message);
|
|
38
|
-
err.code = code || 'ProxyError';
|
|
39
|
-
return err;
|
|
40
|
-
};
|
|
37
|
+
const proxyError = (message, code) => (0, errors_js_1.createImapError)(message, code || 'ProxyError');
|
|
41
38
|
// URL userinfo is percent-encoded, so it has to be decoded before it can be used as credentials.
|
|
42
39
|
// A password containing a bare '%' is not valid percent-encoding and makes decodeURIComponent
|
|
43
40
|
// throw, so such values are used as they came in rather than failing the connection.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.searchCompiler = void 0;
|
|
5
5
|
const tools_js_1 = require("./tools.js");
|
|
6
|
+
const errors_js_1 = require("./errors.js");
|
|
6
7
|
// Matches any character outside the ASCII range
|
|
7
8
|
const UNICODE_PATTERN = /[^\x00-\x7F]/;
|
|
8
9
|
/**
|
|
@@ -104,9 +105,7 @@ let processDateField = (attributes, term, value) => {
|
|
|
104
105
|
* @param message - Error message
|
|
105
106
|
*/
|
|
106
107
|
let fail = (code, message) => {
|
|
107
|
-
|
|
108
|
-
error.code = code;
|
|
109
|
-
throw error;
|
|
108
|
+
throw (0, errors_js_1.createImapError)(message, code);
|
|
110
109
|
};
|
|
111
110
|
/**
|
|
112
111
|
* Checks whether any search value was compiled into a literal, which only
|
package/dist/cjs/tools.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Transform } from 'node:stream';
|
|
2
2
|
import type { ImapFlow } from './imap-flow.js';
|
|
3
|
-
import { type ConnectionErrorSite, type ImapFlowError } from './errors.js';
|
|
3
|
+
import { ImapFlowErrorCode, type ConnectionErrorSite, type ImapFlowError } from './errors.js';
|
|
4
4
|
import type { ImapAttributeList, ImapAttributeNode, ImapResponse } from './handler/types.js';
|
|
5
5
|
import type { FetchMessageObject, ListResponse, ListTreeResponse, MailboxObject, MessageEnvelopeObject, MessageStructureObject, ImapFlowEvents, StatusQuery } from './types.js';
|
|
6
6
|
export { AuthenticationFailure } from './errors.js';
|
|
@@ -33,7 +33,7 @@ export type CharsetDecoder = Transform & {
|
|
|
33
33
|
* @param meta - Fields to stamp on the error
|
|
34
34
|
* @returns The stamped error
|
|
35
35
|
*/
|
|
36
|
-
export declare function buildConnectionError(cid: string, code:
|
|
36
|
+
export declare function buildConnectionError(cid: string, code: ImapFlowErrorCode, message: string, meta?: ConnectionErrorSite | undefined): ImapFlowError;
|
|
37
37
|
/**
|
|
38
38
|
* Re-stamps an existing connection error for a different rejection site.
|
|
39
39
|
*
|
package/dist/cjs/tools.js
CHANGED
|
@@ -146,13 +146,7 @@ const CONNECTION_ERROR_SITE_KEYS = ['rejectedFrom', 'command', 'path'];
|
|
|
146
146
|
* @returns The stamped error
|
|
147
147
|
*/
|
|
148
148
|
function buildConnectionError(cid, code, message, meta) {
|
|
149
|
-
|
|
150
|
-
error.code = code;
|
|
151
|
-
error.cid = cid;
|
|
152
|
-
if (meta) {
|
|
153
|
-
Object.assign(error, meta);
|
|
154
|
-
}
|
|
155
|
-
return error;
|
|
149
|
+
return (0, errors_js_1.createImapError)(message, code, { cid, ...meta });
|
|
156
150
|
}
|
|
157
151
|
/**
|
|
158
152
|
* Re-stamps an existing connection error for a different rejection site.
|
|
@@ -174,6 +168,7 @@ function buildConnectionError(cid, code, message, meta) {
|
|
|
174
168
|
* @returns A separate error describing the same failure at the new site
|
|
175
169
|
*/
|
|
176
170
|
function restampConnectionError(err, meta) {
|
|
171
|
+
// the original code is carried over unchanged, whatever its origin
|
|
177
172
|
let error = buildConnectionError(err.cid, err.code, err.message, err);
|
|
178
173
|
for (let key of CONNECTION_ERROR_SITE_KEYS) {
|
|
179
174
|
delete error[key];
|
|
@@ -527,12 +522,13 @@ function getTextValues(attributes) {
|
|
|
527
522
|
* @param response - Parsed IMAP server response
|
|
528
523
|
* @returns Compiled response text, or false if no response
|
|
529
524
|
*/
|
|
525
|
+
// Async although nothing in it awaits any more: `imapflow/lib/tools` deep imports may chain on the promise
|
|
530
526
|
async function getErrorText(response) {
|
|
531
527
|
if (!response) {
|
|
532
528
|
return false;
|
|
533
529
|
}
|
|
534
530
|
try {
|
|
535
|
-
return (
|
|
531
|
+
return (0, imap_handler_js_1.compiler)(response).toString();
|
|
536
532
|
}
|
|
537
533
|
catch {
|
|
538
534
|
// The wire encoder refuses values that cannot be expressed as a valid IMAP
|
|
@@ -541,7 +537,7 @@ async function getErrorText(response) {
|
|
|
541
537
|
// tolerates stray bytes inside an OK/NO/BAD atom, and those bytes then have
|
|
542
538
|
// no valid re-encoding. This text is diagnostic, so fall back to the logging
|
|
543
539
|
// encoder rather than replacing the server's error with an encoding failure.
|
|
544
|
-
return (
|
|
540
|
+
return (0, imap_handler_js_1.compiler)(response, { isLogging: true }).toString();
|
|
545
541
|
}
|
|
546
542
|
}
|
|
547
543
|
/**
|
|
@@ -755,6 +751,7 @@ function mergeFetchRows(rows) {
|
|
|
755
751
|
* @param connection - Connection the response arrived on, decodes Gmail labels like mailbox names
|
|
756
752
|
* @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
|
|
757
753
|
*/
|
|
754
|
+
// Async although nothing in it awaits any more: `imapflow/lib/tools` deep imports may chain on the promise
|
|
758
755
|
async function formatMessageResponse(untagged, mailbox, idHashAlgorithm, connection) {
|
|
759
756
|
let map = {};
|
|
760
757
|
// The sequence number indexes into mailbox state, so an unusable one is dropped rather
|
|
@@ -777,9 +774,9 @@ async function formatMessageResponse(untagged, mailbox, idHashAlgorithm, connect
|
|
|
777
774
|
let attribute = attributes[i];
|
|
778
775
|
if (i % 2 === 0) {
|
|
779
776
|
origin = false;
|
|
780
|
-
key = (
|
|
777
|
+
key = (0, imap_handler_js_1.compiler)({
|
|
781
778
|
attributes: [attribute]
|
|
782
|
-
})
|
|
779
|
+
})
|
|
783
780
|
.toString()
|
|
784
781
|
.toLowerCase()
|
|
785
782
|
.replace(/<(\d+)(\.\d+)?>$/, (match, start) => {
|
|
@@ -19,4 +19,4 @@ export interface AppendResult extends AppendResponseObject {
|
|
|
19
19
|
* @returns Append result with UID info if available, or undefined if preconditions not met
|
|
20
20
|
* @throws {Error} If the APPEND command fails or message exceeds APPENDLIMIT
|
|
21
21
|
*/
|
|
22
|
-
export default function append(connection: ImapFlow, destination: string | string[], content: Buffer | string, flags?: string | string[] | undefined, idate?: Date | string | false | undefined): Promise<AppendResult | undefined>;
|
|
22
|
+
export default function append(connection: ImapFlow, destination: string | string[], content: Buffer | Uint8Array | string, flags?: string | string[] | undefined, idate?: Date | string | false | undefined): Promise<AppendResult | undefined>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatFlag, canUseFlag, formatDateTime, normalizePath, encodePath, comparePaths, parseBigIntValue, parseUintValue, MAX_UINT32_DIGITS, getSelectedMailbox, emitSafe, isAuthenticatedState, reportCommandError, logConnectionError } from '../tools.js';
|
|
2
|
+
import { createImapError } from '../errors.js';
|
|
2
3
|
/**
|
|
3
4
|
* Appends a message to a mailbox.
|
|
4
5
|
*
|
|
@@ -15,14 +16,24 @@ export default async function append(connection, destination, content, flags, id
|
|
|
15
16
|
// nothing to do here
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
19
|
+
// The literal size marker is announced from these bytes, so anything that is not
|
|
20
|
+
// bytes (a Readable, a plain object) is refused here instead of desyncing the session
|
|
21
|
+
let message;
|
|
18
22
|
if (typeof content === 'string') {
|
|
19
|
-
|
|
23
|
+
message = Buffer.from(content);
|
|
24
|
+
}
|
|
25
|
+
else if (content instanceof Uint8Array) {
|
|
26
|
+
// a Buffer as is, any other Uint8Array as a view over the same memory, not a copy
|
|
27
|
+
message = Buffer.isBuffer(content) ? content : Buffer.from(content.buffer, content.byteOffset, content.byteLength);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
throw createImapError('Message content must be a Buffer, Uint8Array or string', 'InvalidMessageContent');
|
|
20
31
|
}
|
|
21
32
|
// APPENDLIMIT capability (RFC 7889): server may advertise the maximum message
|
|
22
33
|
// size it accepts. Check before sending to avoid a wasted round-trip.
|
|
23
34
|
if (connection.capabilities.has('APPENDLIMIT')) {
|
|
24
35
|
let appendLimit = connection.capabilities.get('APPENDLIMIT');
|
|
25
|
-
if (typeof appendLimit === 'number' && appendLimit <
|
|
36
|
+
if (typeof appendLimit === 'number' && appendLimit < message.length) {
|
|
26
37
|
let err = new Error('Message content too big for APPENDLIMIT=' + appendLimit);
|
|
27
38
|
err.serverResponseCode = 'APPENDLIMIT';
|
|
28
39
|
throw err;
|
|
@@ -79,9 +90,9 @@ export default async function append(connection, destination, content, flags, id
|
|
|
79
90
|
// Regular literals cannot contain NUL bytes per the IMAP grammar.
|
|
80
91
|
let isLiteral8 = false;
|
|
81
92
|
if (connection.capabilities.has('BINARY') && !connection.disableBinary) {
|
|
82
|
-
isLiteral8 =
|
|
93
|
+
isLiteral8 = message.indexOf(0) >= 0;
|
|
83
94
|
}
|
|
84
|
-
attributes.push({ type: 'LITERAL', value:
|
|
95
|
+
attributes.push({ type: 'LITERAL', value: message, isLiteral8 });
|
|
85
96
|
let map = { destination };
|
|
86
97
|
if (connection.mailbox && connection.mailbox.path) {
|
|
87
98
|
map.path = connection.mailbox.path;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createImapError } from '../errors.js';
|
|
1
2
|
/**
|
|
2
3
|
* Requests DEFLATE compression from the server.
|
|
3
4
|
*
|
|
@@ -33,8 +34,7 @@ export default async function compress(connection) {
|
|
|
33
34
|
// back its unconsumed tail on unpipe so the transport can feed it into the
|
|
34
35
|
// inflater - not something a command module can reach from here.
|
|
35
36
|
if (response.hasTrailingData) {
|
|
36
|
-
let error =
|
|
37
|
-
error.code = 'COMPRESS_TRAILING_DATA';
|
|
37
|
+
let error = createImapError('Server sent data between the COMPRESS response and the compression layer switch', 'COMPRESS_TRAILING_DATA');
|
|
38
38
|
connection.log.error({ err: error, cid: connection.id });
|
|
39
39
|
// Schedule the close before releasing parser backpressure, so the buffered
|
|
40
40
|
// deflate-framed bytes cannot settle anything before teardown begins. This is
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { guardedPromise, hasCapability, isServerRefusal, logConnectionError, restampConnectionError, unrefTimer, clearTimer, getSelectedMailbox } from '../tools.js';
|
|
2
|
+
import { createImapError } from '../errors.js';
|
|
2
3
|
const NOOP_INTERVAL = 2 * 60 * 1000;
|
|
3
4
|
/**
|
|
4
5
|
* Marks the connection as idling on behalf of one session and returns a release function.
|
|
@@ -189,9 +190,7 @@ async function pollOnce(connection, session) {
|
|
|
189
190
|
highestModseq: true
|
|
190
191
|
});
|
|
191
192
|
if (!status) {
|
|
192
|
-
|
|
193
|
-
err.code = 'PollFailed';
|
|
194
|
-
throw err;
|
|
193
|
+
throw createImapError('STATUS poll failed', 'PollFailed');
|
|
195
194
|
}
|
|
196
195
|
break;
|
|
197
196
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { encodePath, normalizePath, buildStatusQueryAttributes, isRev2Active, isAuthenticatedState, emitSafe, getSelectedMailbox, logConnectionError } from '../tools.js';
|
|
2
2
|
import { parseStatusList } from './status-fields.js';
|
|
3
|
+
import { createImapError } from '../errors.js';
|
|
3
4
|
// STATUS fields that also refresh the live mailbox state when the queried mailbox is the
|
|
4
5
|
// currently selected one. Keyed by the output property name parseStatusList() reports.
|
|
5
6
|
const MAILBOX_UPDATERS = {
|
|
@@ -96,10 +97,7 @@ export default async function status(connection, path, query) {
|
|
|
96
97
|
return false;
|
|
97
98
|
});
|
|
98
99
|
if (folders && !folders.length) {
|
|
99
|
-
|
|
100
|
-
error.code = 'NotFound';
|
|
101
|
-
error.response = err;
|
|
102
|
-
throw error;
|
|
100
|
+
throw createImapError(`Mailbox doesn't exist: ${path}`, 'NotFound', { response: err });
|
|
103
101
|
}
|
|
104
102
|
}
|
|
105
103
|
connection.log.warn({ err, cid: connection.id });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { clearTimer } from './tools.js';
|
|
2
|
+
import { createImapError } from './errors.js';
|
|
2
3
|
// Default upper bound for establishing a usable transport, including DNS and proxy negotiation.
|
|
3
4
|
export const CONNECT_TIMEOUT = 90 * 1000;
|
|
4
5
|
/**
|
|
@@ -28,10 +29,7 @@ export class ConnectionDeadline {
|
|
|
28
29
|
* @returns The shared `CONNECT_TIMEOUT` error.
|
|
29
30
|
*/
|
|
30
31
|
error() {
|
|
31
|
-
|
|
32
|
-
err.code = 'CONNECT_TIMEOUT';
|
|
33
|
-
err.details = { connectionTimeout: this.timeout };
|
|
34
|
-
return err;
|
|
32
|
+
return createImapError('Failed to establish connection in required time', 'CONNECT_TIMEOUT', { details: { connectionTimeout: this.timeout } });
|
|
35
33
|
}
|
|
36
34
|
/**
|
|
37
35
|
* Maps a dependency's own expiry onto the shared `CONNECT_TIMEOUT` shape, so callers see one
|