imapflow 1.2.8 → 1.2.10
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/.ncurc.js +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +36 -58
- package/eslint.config.js +18 -16
- package/lib/charsets.js +15 -0
- package/lib/commands/append.js +62 -54
- package/lib/commands/authenticate.js +99 -52
- package/lib/commands/capability.js +12 -2
- package/lib/commands/close.js +11 -1
- package/lib/commands/compress.js +10 -1
- package/lib/commands/copy.js +18 -1
- package/lib/commands/create.js +15 -2
- package/lib/commands/delete.js +10 -1
- package/lib/commands/enable.js +12 -1
- package/lib/commands/expunge.js +18 -2
- package/lib/commands/fetch.js +39 -4
- package/lib/commands/id.js +22 -3
- package/lib/commands/idle.js +39 -4
- package/lib/commands/list.js +86 -48
- package/lib/commands/login.js +12 -1
- package/lib/commands/logout.js +11 -2
- package/lib/commands/move.js +17 -1
- package/lib/commands/namespace.js +32 -2
- package/lib/commands/noop.js +6 -1
- package/lib/commands/quota.js +33 -14
- package/lib/commands/rename.js +13 -1
- package/lib/commands/search.js +16 -1
- package/lib/commands/select.js +76 -33
- package/lib/commands/starttls.js +6 -1
- package/lib/commands/status.js +64 -52
- package/lib/commands/store.js +27 -4
- package/lib/commands/subscribe.js +7 -1
- package/lib/commands/unsubscribe.js +7 -1
- package/lib/handler/imap-compiler.js +44 -2
- package/lib/handler/imap-formal-syntax.js +51 -3
- package/lib/handler/imap-handler.js +8 -0
- package/lib/handler/imap-parser.js +23 -2
- package/lib/handler/imap-stream.js +84 -31
- package/lib/handler/parser-instance.js +61 -1
- package/lib/handler/token-parser.js +66 -9
- package/lib/imap-commands.js +11 -0
- package/lib/imap-flow.d.ts +6 -0
- package/lib/imap-flow.js +175 -46
- package/lib/jp-decoder.js +10 -0
- package/lib/limited-passthrough.js +12 -5
- package/lib/proxy-connection.js +18 -12
- package/lib/search-compiler.js +3 -11
- package/lib/special-use.js +23 -16
- package/lib/tools.js +218 -13
- package/package.json +5 -17
- package/test/commands-integration-test.js +33 -0
- package/test/connection-edge-cases-test.js +105 -0
- package/test/special-use-test.js +32 -0
- package/.babelrc +0 -6
- package/.eslintrc +0 -16
- package/assets/favicon.ico +0 -0
- package/jsdoc.json +0 -28
package/lib/imap-flow.js
CHANGED
|
@@ -65,7 +65,7 @@ const states = {
|
|
|
65
65
|
* @property {Set<string>} permanentFlags A Set of flags available to use in this mailbox. If it is not set or includes special flag "\\\*" then any flag can be used.
|
|
66
66
|
* @property {String} [mailboxId] unique mailbox ID if server has `OBJECTID` extension enabled
|
|
67
67
|
* @property {BigInt} [highestModseq] latest known modseq value if server has CONDSTORE or XYMHIGHESTMODSEQ enabled
|
|
68
|
-
* @property {
|
|
68
|
+
* @property {Boolean} [noModseq] if true then the server doesn't support the persistent storage of mod-sequences for the mailbox
|
|
69
69
|
* @property {BigInt} uidValidity Mailbox `UIDVALIDITY` value
|
|
70
70
|
* @property {Number} uidNext Next predicted UID
|
|
71
71
|
* @property {Number} exists Messages in this folder
|
|
@@ -308,7 +308,7 @@ class ImapFlow extends EventEmitter {
|
|
|
308
308
|
this.commandParts = [];
|
|
309
309
|
|
|
310
310
|
/**
|
|
311
|
-
* Active IMAP capabilities. Value is either `true` for
|
|
311
|
+
* Active IMAP capabilities. Value is either `true` for toggleable capabilities (eg. `UIDPLUS`)
|
|
312
312
|
* or a number for capabilities with a value (eg. `APPENDLIMIT`)
|
|
313
313
|
* @type {Map<string, boolean|number>}
|
|
314
314
|
*/
|
|
@@ -372,10 +372,12 @@ class ImapFlow extends EventEmitter {
|
|
|
372
372
|
|
|
373
373
|
this.disableBinary = !!this.options.disableBinary;
|
|
374
374
|
|
|
375
|
-
// Named error handler for proper cleanup
|
|
375
|
+
// Named error handler for proper cleanup. Certain error codes represent
|
|
376
|
+
// expected socket/network issues (buffer exhaustion, connection reset, broken pipe,
|
|
377
|
+
// timeout, unreachable host) that just need a silent connection close rather
|
|
378
|
+
// than emitting an error event to the caller.
|
|
376
379
|
this._streamerErrorHandler = err => {
|
|
377
380
|
if (['Z_BUF_ERROR', 'ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EHOSTUNREACH'].includes(err.code)) {
|
|
378
|
-
// just close the connection, usually nothing but noise
|
|
379
381
|
this.closeAfter();
|
|
380
382
|
return;
|
|
381
383
|
}
|
|
@@ -430,6 +432,9 @@ class ImapFlow extends EventEmitter {
|
|
|
430
432
|
return;
|
|
431
433
|
}
|
|
432
434
|
|
|
435
|
+
// Append CRLF only to the final part of a command. When sending literals,
|
|
436
|
+
// commandParts holds the remaining parts (literal data, continuation); the CRLF
|
|
437
|
+
// delimiter is only added when no more parts remain (the command is complete).
|
|
433
438
|
let addLineBreak = !this.commandParts.length;
|
|
434
439
|
if (typeof chunk === 'string') {
|
|
435
440
|
if (addLineBreak) {
|
|
@@ -460,6 +465,14 @@ class ImapFlow extends EventEmitter {
|
|
|
460
465
|
this.writeSocket.write(chunk);
|
|
461
466
|
}
|
|
462
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Returns byte counters for the current connection.
|
|
470
|
+
*
|
|
471
|
+
* @param {Boolean} [reset] If `true` then resets the byte counters after returning the current values
|
|
472
|
+
* @returns {Object} Byte counters
|
|
473
|
+
* @returns {Number} return.sent Bytes sent to server
|
|
474
|
+
* @returns {Number} return.received Bytes received from server
|
|
475
|
+
*/
|
|
463
476
|
stats(reset) {
|
|
464
477
|
let result = {
|
|
465
478
|
sent: this.writeBytesCounter || 0,
|
|
@@ -476,6 +489,11 @@ class ImapFlow extends EventEmitter {
|
|
|
476
489
|
return result;
|
|
477
490
|
}
|
|
478
491
|
|
|
492
|
+
// Compiles and sends an IMAP command to the server. The command is compiled
|
|
493
|
+
// twice: once as an array (for sending, with literal data split into parts)
|
|
494
|
+
// and once as a string (for logging, with sensitive data masked).
|
|
495
|
+
// When LITERAL- or LITERAL+ extensions are available, the compiler can use
|
|
496
|
+
// non-synchronizing literals to avoid waiting for server "+" continuation.
|
|
479
497
|
async send(data) {
|
|
480
498
|
if (this.state === this.states.LOGOUT) {
|
|
481
499
|
// already logged out
|
|
@@ -491,12 +509,17 @@ class ImapFlow extends EventEmitter {
|
|
|
491
509
|
return;
|
|
492
510
|
}
|
|
493
511
|
|
|
512
|
+
// Compile with asArray=true: splits output into parts for literal handling.
|
|
513
|
+
// First part is the command text up to the first literal, remaining parts
|
|
514
|
+
// are stored in this.commandParts and sent after server "+" continuations.
|
|
494
515
|
let compiled = await compiler(data, {
|
|
495
516
|
asArray: true,
|
|
496
517
|
literalMinus: this.capabilities.has('LITERAL-') || this.capabilities.has('LITERAL+')
|
|
497
518
|
});
|
|
498
519
|
this.commandParts = compiled;
|
|
499
520
|
|
|
521
|
+
// Compile again for logging with isLogging=true: masks sensitive values
|
|
522
|
+
// like passwords while producing a human-readable command string
|
|
500
523
|
let logCompiled = await compiler(data, {
|
|
501
524
|
isLogging: true
|
|
502
525
|
});
|
|
@@ -505,6 +528,9 @@ class ImapFlow extends EventEmitter {
|
|
|
505
528
|
|
|
506
529
|
this.log.debug({ src: 'c', msg: logCompiled.toString(), cid: this.id, comment: options.comment });
|
|
507
530
|
|
|
531
|
+
// Send the first part (command text). If there are literal parts,
|
|
532
|
+
// the server will respond with "+" continuations and reader() will
|
|
533
|
+
// send each remaining part from this.commandParts.
|
|
508
534
|
this.write(this.commandParts.shift());
|
|
509
535
|
|
|
510
536
|
if (typeof options.onSend === 'function') {
|
|
@@ -553,20 +579,29 @@ class ImapFlow extends EventEmitter {
|
|
|
553
579
|
});
|
|
554
580
|
}
|
|
555
581
|
|
|
582
|
+
// Resolves the handler for an untagged server response. IMAP untagged responses
|
|
583
|
+
// come in two forms:
|
|
584
|
+
// * CAPABILITY ... (keyword as command)
|
|
585
|
+
// * 42 FETCH (...) (numeric prefix + keyword)
|
|
586
|
+
// For numeric-prefixed responses, we extract the keyword (FETCH, EXISTS, EXPUNGE, etc.)
|
|
587
|
+
// and look up the handler by that keyword instead.
|
|
588
|
+
// Handler priority: command-specific handlers (registered per exec() call) take
|
|
589
|
+
// precedence over global handlers (registered on the connection).
|
|
556
590
|
getUntaggedHandler(command, attributes) {
|
|
557
591
|
if (/^[0-9]+$/.test(command)) {
|
|
558
592
|
let type = attributes && attributes.length && typeof attributes[0].value === 'string' ? attributes[0].value.toUpperCase() : false;
|
|
559
593
|
if (type) {
|
|
560
|
-
// EXISTS, EXPUNGE, RECENT, FETCH etc
|
|
561
594
|
command = type;
|
|
562
595
|
}
|
|
563
596
|
}
|
|
564
597
|
|
|
565
598
|
command = command.toUpperCase().trim();
|
|
599
|
+
// Check command-specific handler first (registered in exec() options.untagged)
|
|
566
600
|
if (this.currentRequest && this.currentRequest.options && this.currentRequest.options.untagged && this.currentRequest.options.untagged[command]) {
|
|
567
601
|
return this.currentRequest.options.untagged[command];
|
|
568
602
|
}
|
|
569
603
|
|
|
604
|
+
// Fall back to global handler (e.g., for CAPABILITY, BYE, etc.)
|
|
570
605
|
if (this.untaggedHandlers[command]) {
|
|
571
606
|
return this.untaggedHandlers[command];
|
|
572
607
|
}
|
|
@@ -618,12 +653,16 @@ class ImapFlow extends EventEmitter {
|
|
|
618
653
|
this.log.debug({ src: 's', msg: logCompiled.toString(), cid: this.id, nullBytesRemoved: parsed.nullBytesRemoved });
|
|
619
654
|
}
|
|
620
655
|
|
|
656
|
+
// IMAP "+" (continuation request) handling. The server sends "+" in two cases:
|
|
657
|
+
// 1. During IDLE or AUTHENTICATE, where a custom handler (onPlusTag) processes it
|
|
658
|
+
// 2. During literal data transfer, where we send the next queued literal chunk
|
|
621
659
|
if (parsed.tag === '+' && this.currentRequest && this.currentRequest.options && typeof this.currentRequest.options.onPlusTag === 'function') {
|
|
622
660
|
await this.currentRequest.options.onPlusTag(parsed);
|
|
623
661
|
data.next();
|
|
624
662
|
continue;
|
|
625
663
|
}
|
|
626
664
|
|
|
665
|
+
// Server acknowledged our literal size with "+", send the actual literal data
|
|
627
666
|
if (parsed.tag === '+' && this.commandParts.length) {
|
|
628
667
|
let content = this.commandParts.shift();
|
|
629
668
|
this.write(content);
|
|
@@ -706,8 +745,9 @@ class ImapFlow extends EventEmitter {
|
|
|
706
745
|
|
|
707
746
|
let throttleDelay = false;
|
|
708
747
|
|
|
709
|
-
// MS365 throttling
|
|
710
|
-
//
|
|
748
|
+
// MS365 throttling detection: Office 365 returns BAD with a human-readable
|
|
749
|
+
// backoff time when rate limits are hit. Parse the delay from the response text.
|
|
750
|
+
// Example: "tag BAD Request is throttled. Suggested Backoff Time: 92415 milliseconds"
|
|
711
751
|
if (/Request is throttled/i.test(txt) && /Backoff Time/i.test(txt)) {
|
|
712
752
|
let throttlingMatch = txt.match(/Backoff Time[:=\s]+(\d+)/i);
|
|
713
753
|
if (throttlingMatch && throttlingMatch[1] && !isNaN(throttlingMatch[1])) {
|
|
@@ -722,7 +762,8 @@ class ImapFlow extends EventEmitter {
|
|
|
722
762
|
|
|
723
763
|
let delayResponse = throttleDelay;
|
|
724
764
|
if (delayResponse > 5 * 60 * 1000) {
|
|
725
|
-
//
|
|
765
|
+
// Cap wait at 5 minutes to avoid hanging connections indefinitely.
|
|
766
|
+
// The server-suggested delay can be very large.
|
|
726
767
|
delayResponse = 5 * 60 * 1000;
|
|
727
768
|
}
|
|
728
769
|
|
|
@@ -756,6 +797,10 @@ class ImapFlow extends EventEmitter {
|
|
|
756
797
|
}
|
|
757
798
|
|
|
758
799
|
setEventHandlers() {
|
|
800
|
+
// Bind the 'readable' event to kick off the reader loop.
|
|
801
|
+
// The `this.reading` flag acts as a concurrency guard: if reader()
|
|
802
|
+
// is already running, new 'readable' events are ignored. The reader
|
|
803
|
+
// loop will keep draining data until the stream returns null.
|
|
759
804
|
this.socketReadable = () => {
|
|
760
805
|
if (!this.reading) {
|
|
761
806
|
this.reading = true;
|
|
@@ -912,7 +957,12 @@ class ImapFlow extends EventEmitter {
|
|
|
912
957
|
return; // was not able to negotiate compression
|
|
913
958
|
}
|
|
914
959
|
|
|
915
|
-
//
|
|
960
|
+
// Set up DEFLATE compression (RFC 4978). After COMPRESS is negotiated,
|
|
961
|
+
// all data in both directions is wrapped in a zlib DEFLATE stream.
|
|
962
|
+
// The incoming pipeline becomes: socket -> inflate -> streamer (parser).
|
|
963
|
+
// The outgoing pipeline uses a manual pump (see readNext below) instead
|
|
964
|
+
// of a normal pipe, because we need to flush after every IMAP command
|
|
965
|
+
// to ensure the server receives complete commands promptly.
|
|
916
966
|
this._deflate = zlib.createDeflateRaw({
|
|
917
967
|
windowBits: 15,
|
|
918
968
|
level: zlib.constants.Z_DEFAULT_COMPRESSION, // Use default compression level (6)
|
|
@@ -924,7 +974,8 @@ class ImapFlow extends EventEmitter {
|
|
|
924
974
|
chunkSize: 16 * 1024 // Process in 16KB chunks to prevent CPU blocking
|
|
925
975
|
});
|
|
926
976
|
|
|
927
|
-
//
|
|
977
|
+
// Reroute incoming data through inflate: socket -> inflate -> streamer.
|
|
978
|
+
// The streamer's compress flag tells it to expect deflated framing.
|
|
928
979
|
this.socket.unpipe(this.streamer);
|
|
929
980
|
this.streamer.compress = true;
|
|
930
981
|
this.socket.pipe(this._inflate).pipe(this.streamer);
|
|
@@ -932,7 +983,10 @@ class ImapFlow extends EventEmitter {
|
|
|
932
983
|
this.streamer.emit('error', err);
|
|
933
984
|
});
|
|
934
985
|
|
|
935
|
-
//
|
|
986
|
+
// For outgoing data, replace the writeSocket with a PassThrough buffer.
|
|
987
|
+
// We can't pipe writeSocket -> deflate -> socket directly because we need
|
|
988
|
+
// to call deflate.flush() after each IMAP command to push all pending
|
|
989
|
+
// compressed bytes to the server immediately (IMAP is request-response).
|
|
936
990
|
this.writeSocket = new PassThrough({
|
|
937
991
|
highWaterMark: 64 * 1024 // 64KB buffer limit to prevent excessive memory usage
|
|
938
992
|
});
|
|
@@ -953,8 +1007,9 @@ class ImapFlow extends EventEmitter {
|
|
|
953
1007
|
get: () => !this.socket || this.socket.destroyed
|
|
954
1008
|
});
|
|
955
1009
|
|
|
956
|
-
//
|
|
957
|
-
//
|
|
1010
|
+
// Manual pump loop: reads chunks from writeSocket, pushes them into
|
|
1011
|
+
// deflate, and flushes when the buffer is drained. This ensures each
|
|
1012
|
+
// IMAP command is fully compressed and flushed to the socket immediately.
|
|
958
1013
|
let reading = false;
|
|
959
1014
|
let processedChunks = 0;
|
|
960
1015
|
let readNext = async () => {
|
|
@@ -963,7 +1018,7 @@ class ImapFlow extends EventEmitter {
|
|
|
963
1018
|
processedChunks = 0;
|
|
964
1019
|
|
|
965
1020
|
let chunk;
|
|
966
|
-
while ((chunk = this.writeSocket.read()) !== null) {
|
|
1021
|
+
while (this.writeSocket && (chunk = this.writeSocket.read()) !== null) {
|
|
967
1022
|
if (this._deflate && this._deflate.write(chunk) === false) {
|
|
968
1023
|
return this._deflate.once('drain', readNext);
|
|
969
1024
|
}
|
|
@@ -972,6 +1027,9 @@ class ImapFlow extends EventEmitter {
|
|
|
972
1027
|
processedChunks++;
|
|
973
1028
|
if (processedChunks % 100 === 0) {
|
|
974
1029
|
await new Promise(resolve => setImmediate(resolve));
|
|
1030
|
+
if (!this.writeSocket) {
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
975
1033
|
}
|
|
976
1034
|
}
|
|
977
1035
|
|
|
@@ -987,17 +1045,21 @@ class ImapFlow extends EventEmitter {
|
|
|
987
1045
|
};
|
|
988
1046
|
|
|
989
1047
|
this.writeSocket.on('readable', () => {
|
|
990
|
-
if (!reading) {
|
|
1048
|
+
if (!reading && this.writeSocket) {
|
|
991
1049
|
readNext();
|
|
992
1050
|
}
|
|
993
1051
|
});
|
|
994
1052
|
this.writeSocket.on('error', err => {
|
|
995
|
-
this.socket
|
|
1053
|
+
if (this.socket) {
|
|
1054
|
+
this.socket.emit('error', err);
|
|
1055
|
+
}
|
|
996
1056
|
});
|
|
997
1057
|
|
|
998
1058
|
this._deflate.pipe(this.socket);
|
|
999
1059
|
this._deflate.on('error', err => {
|
|
1000
|
-
this.socket
|
|
1060
|
+
if (this.socket) {
|
|
1061
|
+
this.socket.emit('error', err);
|
|
1062
|
+
}
|
|
1001
1063
|
});
|
|
1002
1064
|
}
|
|
1003
1065
|
|
|
@@ -1044,6 +1106,9 @@ class ImapFlow extends EventEmitter {
|
|
|
1044
1106
|
return this._failSTARTTLS();
|
|
1045
1107
|
}
|
|
1046
1108
|
|
|
1109
|
+
// STARTTLS upgrade sequence: detach the plain socket from the parser,
|
|
1110
|
+
// wrap it in a TLS socket, then reconnect the new TLS socket to the
|
|
1111
|
+
// parser. The plain socket becomes the underlying transport for TLS.
|
|
1047
1112
|
this.socket.unpipe(this.streamer);
|
|
1048
1113
|
let upgraded = await new Promise((resolve, reject) => {
|
|
1049
1114
|
let socketPlain = this.socket;
|
|
@@ -1092,6 +1157,8 @@ class ImapFlow extends EventEmitter {
|
|
|
1092
1157
|
return this.close();
|
|
1093
1158
|
}
|
|
1094
1159
|
|
|
1160
|
+
// TLS handshake complete. Reconnect the now-encrypted socket
|
|
1161
|
+
// to the IMAP parser stream and record the cipher details.
|
|
1095
1162
|
this.secureConnection = true;
|
|
1096
1163
|
this.upgrading = false;
|
|
1097
1164
|
this.streamer.secureConnection = true;
|
|
@@ -1432,12 +1499,16 @@ class ImapFlow extends EventEmitter {
|
|
|
1432
1499
|
return true;
|
|
1433
1500
|
}
|
|
1434
1501
|
|
|
1502
|
+
// Normalizes a message range from various input formats into an IMAP-compatible
|
|
1503
|
+
// sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
|
|
1504
|
+
// {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
|
|
1435
1505
|
async resolveRange(range, options) {
|
|
1436
1506
|
if (typeof range === 'number' || typeof range === 'bigint') {
|
|
1437
1507
|
range = range.toString();
|
|
1438
1508
|
}
|
|
1439
1509
|
|
|
1440
|
-
//
|
|
1510
|
+
// Replace "*" with the actual message count. Some servers reject bare "*"
|
|
1511
|
+
// in certain commands, and this also forces a sequence query (not UID).
|
|
1441
1512
|
if (range === '*') {
|
|
1442
1513
|
if (!this.mailbox.exists) {
|
|
1443
1514
|
return false;
|
|
@@ -1453,7 +1524,8 @@ class ImapFlow extends EventEmitter {
|
|
|
1453
1524
|
range = range.uid;
|
|
1454
1525
|
options.uid = true;
|
|
1455
1526
|
} else {
|
|
1456
|
-
// resolve
|
|
1527
|
+
// Arbitrary search query object: run SEARCH to resolve it into
|
|
1528
|
+
// a set of UIDs, then pack into a compact range string.
|
|
1457
1529
|
options.uid = true; // force UIDs instead of sequence numbers
|
|
1458
1530
|
range = await this.run('SEARCH', range, options);
|
|
1459
1531
|
if (range && range.length) {
|
|
@@ -1688,7 +1760,8 @@ class ImapFlow extends EventEmitter {
|
|
|
1688
1760
|
this.initialReject = false;
|
|
1689
1761
|
let err = new Error('Unexpected close');
|
|
1690
1762
|
err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
|
|
1691
|
-
//
|
|
1763
|
+
// Reject via setImmediate so the rest of close() cleanup runs first.
|
|
1764
|
+
// The caller's connect() promise will reject after sockets are torn down.
|
|
1692
1765
|
setImmediate(() => reject(err));
|
|
1693
1766
|
}
|
|
1694
1767
|
|
|
@@ -1731,8 +1804,9 @@ class ImapFlow extends EventEmitter {
|
|
|
1731
1804
|
return error;
|
|
1732
1805
|
};
|
|
1733
1806
|
|
|
1734
|
-
// Reject pending requests via setImmediate
|
|
1735
|
-
//
|
|
1807
|
+
// Reject pending requests via setImmediate so this synchronous close()
|
|
1808
|
+
// method finishes first. This prevents unhandled promise rejections that
|
|
1809
|
+
// would occur if we rejected inline before callers set up .catch() handlers.
|
|
1736
1810
|
if (pendingRequests.length) {
|
|
1737
1811
|
let byeReason = this.byeReason;
|
|
1738
1812
|
setImmediate(() => {
|
|
@@ -1809,6 +1883,11 @@ class ImapFlow extends EventEmitter {
|
|
|
1809
1883
|
return;
|
|
1810
1884
|
}
|
|
1811
1885
|
|
|
1886
|
+
// Socket teardown order matters when compression is active:
|
|
1887
|
+
// writeSocket may be a PassThrough (compression) or the raw socket (no compression).
|
|
1888
|
+
// Destroy the underlying socket first, then writeSocket (if different).
|
|
1889
|
+
// The second socket.destroy() block handles the case where writeSocket.destroy()
|
|
1890
|
+
// did not also destroy the underlying socket.
|
|
1812
1891
|
if (this.socket && !this.socket.destroyed && this.writeSocket !== this.socket) {
|
|
1813
1892
|
try {
|
|
1814
1893
|
this.socket.destroy();
|
|
@@ -1834,7 +1913,8 @@ class ImapFlow extends EventEmitter {
|
|
|
1834
1913
|
}
|
|
1835
1914
|
}
|
|
1836
1915
|
|
|
1837
|
-
//
|
|
1916
|
+
// Null out all socket and handler references so the GC can collect
|
|
1917
|
+
// them even if the ImapFlow instance itself is still referenced.
|
|
1838
1918
|
this.socket = null;
|
|
1839
1919
|
this.writeSocket = null;
|
|
1840
1920
|
this._inflate = null;
|
|
@@ -1870,11 +1950,11 @@ class ImapFlow extends EventEmitter {
|
|
|
1870
1950
|
* Returns current quota
|
|
1871
1951
|
*
|
|
1872
1952
|
* @param {String} [path] Optional mailbox path if you want to check quota for specific folder
|
|
1873
|
-
* @returns {Promise<QuotaResponse|Boolean>} Quota information or `false` if
|
|
1953
|
+
* @returns {Promise<QuotaResponse|Boolean>} Quota information or `false` if QUOTA extension is not supported or requested path does not exist
|
|
1874
1954
|
*
|
|
1875
1955
|
* @example
|
|
1876
1956
|
* let quota = await client.getQuota();
|
|
1877
|
-
* console.log(quota.storage.used, quota.storage.
|
|
1957
|
+
* console.log(quota.storage.used, quota.storage.limit)
|
|
1878
1958
|
*/
|
|
1879
1959
|
async getQuota(path) {
|
|
1880
1960
|
path = path || 'INBOX';
|
|
@@ -1938,7 +2018,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1938
2018
|
* @property {String} path mailbox path
|
|
1939
2019
|
* @property {String} name mailbox name (last part of path after delimiter)
|
|
1940
2020
|
* @property {String} delimiter mailbox path delimiter, usually "." or "/"
|
|
1941
|
-
* @property {
|
|
2021
|
+
* @property {Set<string>} flags list of flags for this mailbox
|
|
1942
2022
|
* @property {String} specialUse one of special-use flags (if applicable): "\All", "\Archive", "\Drafts", "\Flagged", "\Junk", "\Sent", "\Trash". Additionally INBOX has non-standard "\Inbox" flag set
|
|
1943
2023
|
* @property {Boolean} listed `true` if mailbox was found from the output of LIST command
|
|
1944
2024
|
* @property {Boolean} subscribed `true` if mailbox was found from the output of LSUB command
|
|
@@ -2206,7 +2286,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2206
2286
|
* Sets flags for a message or message range
|
|
2207
2287
|
*
|
|
2208
2288
|
* @param {SequenceString | Number[] | SearchObject} range Range to filter the messages
|
|
2209
|
-
* @param {string[]} Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
|
|
2289
|
+
* @param {string[]} flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
|
|
2210
2290
|
* @param {Object} [options]
|
|
2211
2291
|
* @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
|
|
2212
2292
|
* @param {BigInt} [options.unchangedSince] If set then only messages with a lower or equal `modseq` value are updated. Ignored if server does not support `CONDSTORE` extension.
|
|
@@ -2240,7 +2320,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2240
2320
|
* Adds flags for a message or message range
|
|
2241
2321
|
*
|
|
2242
2322
|
* @param {SequenceString | Number[] | SearchObject} range Range to filter the messages
|
|
2243
|
-
* @param {string[]} Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
|
|
2323
|
+
* @param {string[]} flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
|
|
2244
2324
|
* @param {Object} [options]
|
|
2245
2325
|
* @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
|
|
2246
2326
|
* @param {BigInt} [options.unchangedSince] If set then only messages with a lower or equal `modseq` value are updated. Ignored if server does not support `CONDSTORE` extension.
|
|
@@ -2274,7 +2354,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2274
2354
|
* Remove specific flags from a message or message range
|
|
2275
2355
|
*
|
|
2276
2356
|
* @param {SequenceString | Number[] | SearchObject} range Range to filter the messages
|
|
2277
|
-
* @param {string[]} Array of flags to remove. Only flags that are permitted to set are used, other flags are ignored
|
|
2357
|
+
* @param {string[]} flags Array of flags to remove. Only flags that are permitted to set are used, other flags are ignored
|
|
2278
2358
|
* @param {Object} [options]
|
|
2279
2359
|
* @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
|
|
2280
2360
|
* @param {BigInt} [options.unchangedSince] If set then only messages with a lower or equal `modseq` value are updated. Ignored if server does not support `CONDSTORE` extension.
|
|
@@ -2308,7 +2388,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2308
2388
|
* Sets a colored flag for an email. Only supported by mail clients like Apple Mail
|
|
2309
2389
|
*
|
|
2310
2390
|
* @param {SequenceString | Number[] | SearchObject} range Range to filter the messages
|
|
2311
|
-
* @param {string} The color to set. One of 'red', 'orange', 'yellow', 'green', 'blue', 'purple', and 'grey'
|
|
2391
|
+
* @param {string} color The color to set. One of 'red', 'orange', 'yellow', 'green', 'blue', 'purple', and 'grey'
|
|
2312
2392
|
* @param {Object} [options]
|
|
2313
2393
|
* @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
|
|
2314
2394
|
* @param {BigInt} [options.unchangedSince] If set then only messages with a lower or equal `modseq` value are updated. Ignored if server does not support `CONDSTORE` extension.
|
|
@@ -2525,10 +2605,13 @@ class ImapFlow extends EventEmitter {
|
|
|
2525
2605
|
* @property {boolean | Object} [source] if `true` then include full message in the response
|
|
2526
2606
|
* @property {Number} [source.start] include full message in the response starting from *start* byte
|
|
2527
2607
|
* @property {Number} [source.maxLength] include full message in the response, up to *maxLength* bytes
|
|
2528
|
-
* @property {
|
|
2608
|
+
* @property {Boolean} [threadId] if `true` then include thread ID in the response (only if server supports either `OBJECTID` or `X-GM-EXT-1` extensions)
|
|
2529
2609
|
* @property {Boolean} [labels] if `true` then include GMail labels in the response (only if server supports `X-GM-EXT-1` extension)
|
|
2530
2610
|
* @property {boolean | string[]} [headers] if `true` then includes full headers of the message in the response. If the value is an array of header keys then includes only headers listed in the array
|
|
2531
2611
|
* @property {string[]} [bodyParts] An array of BODYPART identifiers to include in the response
|
|
2612
|
+
* @property {Boolean} [fast] IMAP macro equivalent to `flags`, `internalDate`, `size`
|
|
2613
|
+
* @property {Boolean} [all] IMAP macro equivalent to `flags`, `internalDate`, `size`, `envelope`
|
|
2614
|
+
* @property {Boolean} [full] IMAP macro equivalent to `flags`, `internalDate`, `size`, `envelope`, `bodyStructure`
|
|
2532
2615
|
*/
|
|
2533
2616
|
|
|
2534
2617
|
/**
|
|
@@ -2584,7 +2667,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2584
2667
|
* @property {Buffer} [source] message source for the requested byte range
|
|
2585
2668
|
* @property {BigInt} [modseq] message Modseq number. Always included if the server supports CONDSTORE extension
|
|
2586
2669
|
* @property {String} [emailId] unique email ID. Always included if server supports `OBJECTID` or `X-GM-EXT-1` extensions
|
|
2587
|
-
* @property {String} [
|
|
2670
|
+
* @property {String} [threadId] unique thread ID. Only present if server supports `OBJECTID` or `X-GM-EXT-1` extension
|
|
2588
2671
|
* @property {Set<string>} [labels] a Set of labels. Only present if server supports `X-GM-EXT-1` extension
|
|
2589
2672
|
* @property {Number} [size] message size
|
|
2590
2673
|
* @property {Set<string>} [flags] a set of message flags
|
|
@@ -2629,6 +2712,11 @@ class ImapFlow extends EventEmitter {
|
|
|
2629
2712
|
return false;
|
|
2630
2713
|
}
|
|
2631
2714
|
|
|
2715
|
+
// Push/pull coordination for the async generator pattern:
|
|
2716
|
+
// The FETCH command handler pushes results into rowQueue via onUntaggedFetch.
|
|
2717
|
+
// The generator consumer pulls via getNext(). The `push` callback bridges the
|
|
2718
|
+
// two: when the consumer is waiting and the queue is empty, `push` is set to
|
|
2719
|
+
// a function that wakes up the consumer when new data arrives.
|
|
2632
2720
|
let finished = false;
|
|
2633
2721
|
let push = false;
|
|
2634
2722
|
let rowQueue = [];
|
|
@@ -2649,7 +2737,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2649
2737
|
return resolve(null);
|
|
2650
2738
|
}
|
|
2651
2739
|
|
|
2652
|
-
//
|
|
2740
|
+
// No data available yet; register a wakeup callback
|
|
2653
2741
|
push = () => {
|
|
2654
2742
|
push = false;
|
|
2655
2743
|
check();
|
|
@@ -2658,6 +2746,10 @@ class ImapFlow extends EventEmitter {
|
|
|
2658
2746
|
check();
|
|
2659
2747
|
});
|
|
2660
2748
|
|
|
2749
|
+
// Fire-and-forget the FETCH command. It runs in the background while
|
|
2750
|
+
// the generator yields results. Each untagged FETCH response is paired
|
|
2751
|
+
// with a `next` callback that acts as backpressure: the FETCH handler
|
|
2752
|
+
// won't process the next response until the consumer calls next().
|
|
2661
2753
|
this.run('FETCH', range, query, {
|
|
2662
2754
|
uid: !!options.uid,
|
|
2663
2755
|
binary: options.binary,
|
|
@@ -2697,6 +2789,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2697
2789
|
|
|
2698
2790
|
if (res !== null) {
|
|
2699
2791
|
yield res.response;
|
|
2792
|
+
// Signal the FETCH handler to process the next untagged response
|
|
2700
2793
|
res.next();
|
|
2701
2794
|
}
|
|
2702
2795
|
}
|
|
@@ -2833,8 +2926,9 @@ class ImapFlow extends EventEmitter {
|
|
|
2833
2926
|
let uid = false;
|
|
2834
2927
|
|
|
2835
2928
|
if (part === '1') {
|
|
2836
|
-
//
|
|
2837
|
-
// the
|
|
2929
|
+
// Special handling for part "1": in single-node emails (no childNodes),
|
|
2930
|
+
// the body is accessed via "TEXT" rather than "1", and headers via
|
|
2931
|
+
// "HEADER" instead of "1.MIME". Check bodyStructure to detect this.
|
|
2838
2932
|
let response = await this.fetchOne(range, { uid: true, bodyStructure: true }, options);
|
|
2839
2933
|
|
|
2840
2934
|
if (!response) {
|
|
@@ -2994,6 +3088,13 @@ class ImapFlow extends EventEmitter {
|
|
|
2994
3088
|
let output;
|
|
2995
3089
|
let fetchAborted = false;
|
|
2996
3090
|
|
|
3091
|
+
// Build a decoder pipeline that progressively transforms the raw FETCH data:
|
|
3092
|
+
// 1. Transfer-encoding decoder (base64 or quoted-printable -> binary)
|
|
3093
|
+
// 2. Format decoder (format=flowed -> plain text, if applicable)
|
|
3094
|
+
// 3. Charset decoder (non-UTF-8 -> UTF-8, for text parts only)
|
|
3095
|
+
// 4. Byte limiter (enforces maxBytes cap)
|
|
3096
|
+
// `stream` is the head of the pipeline (where raw chunks are written),
|
|
3097
|
+
// `output` is the tail (what the caller reads from).
|
|
2997
3098
|
switch (meta.encoding) {
|
|
2998
3099
|
case 'base64':
|
|
2999
3100
|
output = stream = new libbase64.Decoder();
|
|
@@ -3007,7 +3108,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3007
3108
|
|
|
3008
3109
|
let isTextNode = ['text/html', 'text/plain', 'text/x-amp-html'].includes(meta.contentType) || (part === '1' && !meta.contentType);
|
|
3009
3110
|
if ((!meta.disposition || meta.disposition === 'inline') && isTextNode) {
|
|
3010
|
-
// flowed text
|
|
3111
|
+
// RFC 3676 format=flowed text: unwrap soft line breaks
|
|
3011
3112
|
if (meta.flowed) {
|
|
3012
3113
|
let flowDecoder = new FlowedDecoder({
|
|
3013
3114
|
delSp: meta.delSp
|
|
@@ -3018,7 +3119,8 @@ class ImapFlow extends EventEmitter {
|
|
|
3018
3119
|
output = output.pipe(flowDecoder);
|
|
3019
3120
|
}
|
|
3020
3121
|
|
|
3021
|
-
//
|
|
3122
|
+
// Convert non-UTF-8 charsets to UTF-8 via a streaming decoder.
|
|
3123
|
+
// ASCII and UTF-8 need no conversion. Unknown charsets are left as-is.
|
|
3022
3124
|
if (meta.charset && !['ascii', 'usascii', 'utf8'].includes(meta.charset.toLowerCase().replace(/[^a-z0-9]+/g, ''))) {
|
|
3023
3125
|
try {
|
|
3024
3126
|
let decoder = getDecoder(meta.charset);
|
|
@@ -3059,6 +3161,9 @@ class ImapFlow extends EventEmitter {
|
|
|
3059
3161
|
return stream.write(chunk);
|
|
3060
3162
|
};
|
|
3061
3163
|
|
|
3164
|
+
// Fetch remaining chunks in a loop, writing each to the decoder stream.
|
|
3165
|
+
// Stops when the server returns a short chunk (< chunkSize), the byte
|
|
3166
|
+
// limiter is satisfied, or the consumer destroys the output stream.
|
|
3062
3167
|
let fetchAllParts = async () => {
|
|
3063
3168
|
while (hasMore && !limiter.limited && !fetchAborted) {
|
|
3064
3169
|
let { chunk } = await getNextPart();
|
|
@@ -3108,6 +3213,11 @@ class ImapFlow extends EventEmitter {
|
|
|
3108
3213
|
}
|
|
3109
3214
|
};
|
|
3110
3215
|
|
|
3216
|
+
// Kick off the download pipeline asynchronously. The first chunk was
|
|
3217
|
+
// already fetched above (to get metadata); write it to the decoder
|
|
3218
|
+
// stream and then fetch remaining chunks via fetchAllParts().
|
|
3219
|
+
// setImmediate ensures the caller gets the {meta, content} return
|
|
3220
|
+
// value before streaming begins.
|
|
3111
3221
|
setImmediate(() => {
|
|
3112
3222
|
let writeResult;
|
|
3113
3223
|
try {
|
|
@@ -3180,7 +3290,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3180
3290
|
* Fetch multiple attachments as Buffer values
|
|
3181
3291
|
*
|
|
3182
3292
|
* @param {SequenceString} range UID or sequence number for the message to fetch
|
|
3183
|
-
* @param {String} parts A list of bodystructure parts
|
|
3293
|
+
* @param {String[]} parts A list of bodystructure parts
|
|
3184
3294
|
* @param {Object} [options]
|
|
3185
3295
|
* @param {Boolean} [options.uid] If `true` then uses UID number instead of sequence number for `range`
|
|
3186
3296
|
* @returns {Promise<Object>} Download data object
|
|
@@ -3337,11 +3447,14 @@ class ImapFlow extends EventEmitter {
|
|
|
3337
3447
|
return result;
|
|
3338
3448
|
}
|
|
3339
3449
|
|
|
3450
|
+
// Mailbox lock queue processor. Implements a mutex pattern: only one lock
|
|
3451
|
+
// is active at a time. When the active lock is released, the next queued
|
|
3452
|
+
// lock is processed. The `processingLock` flag prevents concurrent runs
|
|
3453
|
+
// of this method (which could happen via setImmediate re-entry from release()).
|
|
3340
3454
|
async processLocks() {
|
|
3341
|
-
// Atomic test-and-set to prevent race condition
|
|
3342
3455
|
const wasProcessing = this.processingLock;
|
|
3343
3456
|
if (wasProcessing) {
|
|
3344
|
-
// Another processor is already running
|
|
3457
|
+
// Another processor is already running; it will pick up new locks
|
|
3345
3458
|
this.log.trace({
|
|
3346
3459
|
msg: 'Mailbox locking queued',
|
|
3347
3460
|
path: this.mailbox && this.mailbox.path,
|
|
@@ -3405,7 +3518,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3405
3518
|
}
|
|
3406
3519
|
|
|
3407
3520
|
if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
|
|
3408
|
-
//
|
|
3521
|
+
// Fast path: mailbox is already selected with the right access mode
|
|
3409
3522
|
this.log.trace({
|
|
3410
3523
|
msg: 'Mailbox lock acquired [existing]',
|
|
3411
3524
|
path,
|
|
@@ -3415,10 +3528,10 @@ class ImapFlow extends EventEmitter {
|
|
|
3415
3528
|
});
|
|
3416
3529
|
this.currentLock = lock;
|
|
3417
3530
|
resolve({ path, release });
|
|
3418
|
-
break; //
|
|
3531
|
+
break; // Stop processing; next lock waits for release()
|
|
3419
3532
|
} else {
|
|
3420
3533
|
try {
|
|
3421
|
-
//
|
|
3534
|
+
// Need to SELECT/EXAMINE a different mailbox
|
|
3422
3535
|
await this.mailboxOpen(path, options);
|
|
3423
3536
|
this.log.trace({
|
|
3424
3537
|
msg: 'Mailbox lock acquired [selected]',
|
|
@@ -3432,6 +3545,9 @@ class ImapFlow extends EventEmitter {
|
|
|
3432
3545
|
break; // Wait for this lock to be released
|
|
3433
3546
|
} catch (err) {
|
|
3434
3547
|
if (err.responseStatus === 'NO') {
|
|
3548
|
+
// SELECT failed with NO -- verify whether the mailbox exists
|
|
3549
|
+
// at all by running LIST. This sets mailboxMissing on the error
|
|
3550
|
+
// so the caller can distinguish "doesn't exist" from other failures.
|
|
3435
3551
|
try {
|
|
3436
3552
|
let folders = await this.run('LIST', '', path, { listOnly: true });
|
|
3437
3553
|
if (!folders || !folders.length) {
|
|
@@ -3458,9 +3574,10 @@ class ImapFlow extends EventEmitter {
|
|
|
3458
3574
|
} finally {
|
|
3459
3575
|
this.processingLock = false;
|
|
3460
3576
|
|
|
3461
|
-
//
|
|
3577
|
+
// New locks may have been queued while we were processing (e.g.,
|
|
3578
|
+
// a lock that failed immediately and the next getMailboxLock call
|
|
3579
|
+
// arrived before we finished). Schedule another run if needed.
|
|
3462
3580
|
if (this.locks.length && !this.currentLock) {
|
|
3463
|
-
// Process any remaining locks
|
|
3464
3581
|
setImmediate(() => {
|
|
3465
3582
|
this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
|
|
3466
3583
|
});
|
|
@@ -3561,6 +3678,14 @@ class ImapFlow extends EventEmitter {
|
|
|
3561
3678
|
return synteticLogger;
|
|
3562
3679
|
}
|
|
3563
3680
|
|
|
3681
|
+
/**
|
|
3682
|
+
* Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
|
|
3683
|
+
* (e.g., STARTTLS) or transferring socket ownership.
|
|
3684
|
+
*
|
|
3685
|
+
* @returns {Object} Socket objects
|
|
3686
|
+
* @returns {Object} return.readSocket The read socket (inflated socket if compression is enabled, raw socket otherwise)
|
|
3687
|
+
* @returns {Object} return.writeSocket The write socket
|
|
3688
|
+
*/
|
|
3564
3689
|
unbind() {
|
|
3565
3690
|
this.socket.unpipe(this.streamer);
|
|
3566
3691
|
if (this._inflate) {
|
|
@@ -3620,6 +3745,9 @@ class ImapFlow extends EventEmitter {
|
|
|
3620
3745
|
* @type {Object}
|
|
3621
3746
|
* @property {String} path mailbox path this event applies to
|
|
3622
3747
|
* @property {Number} seq sequence number of deleted message
|
|
3748
|
+
* @property {Boolean} vanished `true` if message was expunged via VANISHED response
|
|
3749
|
+
* @property {Number} [uid] UID of expunged message (when `vanished` is `true`)
|
|
3750
|
+
* @property {Boolean} [earlier] `true` for VANISHED EARLIER responses
|
|
3623
3751
|
* @example
|
|
3624
3752
|
* client.on('expunge', data=>{
|
|
3625
3753
|
* console.log(`Message #${data.seq} was deleted from "${data.path}"`);
|
|
@@ -3636,6 +3764,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3636
3764
|
* @property {Number} [uid] UID number of updated message (if server provided this value)
|
|
3637
3765
|
* @property {BigInt} [modseq] Updated modseq number for the mailbox (if server provided this value)
|
|
3638
3766
|
* @property {Set<string>} flags A set of all flags for the updated message
|
|
3767
|
+
* @property {String} [flagColor] flag color like "red", or "yellow". Derived from the `flags` Set using Apple Mail color rules
|
|
3639
3768
|
* @example
|
|
3640
3769
|
* client.on('flags', data=>{
|
|
3641
3770
|
* console.log(`Flag set for #${data.seq} is now "${Array.from(data.flags).join(', ')}"`);
|
|
@@ -3671,7 +3800,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3671
3800
|
* @type {Object}
|
|
3672
3801
|
* @example
|
|
3673
3802
|
* client.on('log', entry => {
|
|
3674
|
-
* console.log(`${
|
|
3803
|
+
* console.log(`${entry.cid} ${entry.msg}`);
|
|
3675
3804
|
* });
|
|
3676
3805
|
*/
|
|
3677
3806
|
|