imapflow 1.2.8 → 1.2.9
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 +7 -0
- package/README.md +3 -3
- 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 +164 -42
- 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 +4 -11
- package/test/commands-integration-test.js +33 -0
- package/test/special-use-test.js +32 -0
- 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 () => {
|
|
@@ -1044,6 +1099,9 @@ class ImapFlow extends EventEmitter {
|
|
|
1044
1099
|
return this._failSTARTTLS();
|
|
1045
1100
|
}
|
|
1046
1101
|
|
|
1102
|
+
// STARTTLS upgrade sequence: detach the plain socket from the parser,
|
|
1103
|
+
// wrap it in a TLS socket, then reconnect the new TLS socket to the
|
|
1104
|
+
// parser. The plain socket becomes the underlying transport for TLS.
|
|
1047
1105
|
this.socket.unpipe(this.streamer);
|
|
1048
1106
|
let upgraded = await new Promise((resolve, reject) => {
|
|
1049
1107
|
let socketPlain = this.socket;
|
|
@@ -1092,6 +1150,8 @@ class ImapFlow extends EventEmitter {
|
|
|
1092
1150
|
return this.close();
|
|
1093
1151
|
}
|
|
1094
1152
|
|
|
1153
|
+
// TLS handshake complete. Reconnect the now-encrypted socket
|
|
1154
|
+
// to the IMAP parser stream and record the cipher details.
|
|
1095
1155
|
this.secureConnection = true;
|
|
1096
1156
|
this.upgrading = false;
|
|
1097
1157
|
this.streamer.secureConnection = true;
|
|
@@ -1432,12 +1492,16 @@ class ImapFlow extends EventEmitter {
|
|
|
1432
1492
|
return true;
|
|
1433
1493
|
}
|
|
1434
1494
|
|
|
1495
|
+
// Normalizes a message range from various input formats into an IMAP-compatible
|
|
1496
|
+
// sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
|
|
1497
|
+
// {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
|
|
1435
1498
|
async resolveRange(range, options) {
|
|
1436
1499
|
if (typeof range === 'number' || typeof range === 'bigint') {
|
|
1437
1500
|
range = range.toString();
|
|
1438
1501
|
}
|
|
1439
1502
|
|
|
1440
|
-
//
|
|
1503
|
+
// Replace "*" with the actual message count. Some servers reject bare "*"
|
|
1504
|
+
// in certain commands, and this also forces a sequence query (not UID).
|
|
1441
1505
|
if (range === '*') {
|
|
1442
1506
|
if (!this.mailbox.exists) {
|
|
1443
1507
|
return false;
|
|
@@ -1453,7 +1517,8 @@ class ImapFlow extends EventEmitter {
|
|
|
1453
1517
|
range = range.uid;
|
|
1454
1518
|
options.uid = true;
|
|
1455
1519
|
} else {
|
|
1456
|
-
// resolve
|
|
1520
|
+
// Arbitrary search query object: run SEARCH to resolve it into
|
|
1521
|
+
// a set of UIDs, then pack into a compact range string.
|
|
1457
1522
|
options.uid = true; // force UIDs instead of sequence numbers
|
|
1458
1523
|
range = await this.run('SEARCH', range, options);
|
|
1459
1524
|
if (range && range.length) {
|
|
@@ -1688,7 +1753,8 @@ class ImapFlow extends EventEmitter {
|
|
|
1688
1753
|
this.initialReject = false;
|
|
1689
1754
|
let err = new Error('Unexpected close');
|
|
1690
1755
|
err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
|
|
1691
|
-
//
|
|
1756
|
+
// Reject via setImmediate so the rest of close() cleanup runs first.
|
|
1757
|
+
// The caller's connect() promise will reject after sockets are torn down.
|
|
1692
1758
|
setImmediate(() => reject(err));
|
|
1693
1759
|
}
|
|
1694
1760
|
|
|
@@ -1731,8 +1797,9 @@ class ImapFlow extends EventEmitter {
|
|
|
1731
1797
|
return error;
|
|
1732
1798
|
};
|
|
1733
1799
|
|
|
1734
|
-
// Reject pending requests via setImmediate
|
|
1735
|
-
//
|
|
1800
|
+
// Reject pending requests via setImmediate so this synchronous close()
|
|
1801
|
+
// method finishes first. This prevents unhandled promise rejections that
|
|
1802
|
+
// would occur if we rejected inline before callers set up .catch() handlers.
|
|
1736
1803
|
if (pendingRequests.length) {
|
|
1737
1804
|
let byeReason = this.byeReason;
|
|
1738
1805
|
setImmediate(() => {
|
|
@@ -1809,6 +1876,11 @@ class ImapFlow extends EventEmitter {
|
|
|
1809
1876
|
return;
|
|
1810
1877
|
}
|
|
1811
1878
|
|
|
1879
|
+
// Socket teardown order matters when compression is active:
|
|
1880
|
+
// writeSocket may be a PassThrough (compression) or the raw socket (no compression).
|
|
1881
|
+
// Destroy the underlying socket first, then writeSocket (if different).
|
|
1882
|
+
// The second socket.destroy() block handles the case where writeSocket.destroy()
|
|
1883
|
+
// did not also destroy the underlying socket.
|
|
1812
1884
|
if (this.socket && !this.socket.destroyed && this.writeSocket !== this.socket) {
|
|
1813
1885
|
try {
|
|
1814
1886
|
this.socket.destroy();
|
|
@@ -1834,7 +1906,8 @@ class ImapFlow extends EventEmitter {
|
|
|
1834
1906
|
}
|
|
1835
1907
|
}
|
|
1836
1908
|
|
|
1837
|
-
//
|
|
1909
|
+
// Null out all socket and handler references so the GC can collect
|
|
1910
|
+
// them even if the ImapFlow instance itself is still referenced.
|
|
1838
1911
|
this.socket = null;
|
|
1839
1912
|
this.writeSocket = null;
|
|
1840
1913
|
this._inflate = null;
|
|
@@ -1870,11 +1943,11 @@ class ImapFlow extends EventEmitter {
|
|
|
1870
1943
|
* Returns current quota
|
|
1871
1944
|
*
|
|
1872
1945
|
* @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
|
|
1946
|
+
* @returns {Promise<QuotaResponse|Boolean>} Quota information or `false` if QUOTA extension is not supported or requested path does not exist
|
|
1874
1947
|
*
|
|
1875
1948
|
* @example
|
|
1876
1949
|
* let quota = await client.getQuota();
|
|
1877
|
-
* console.log(quota.storage.used, quota.storage.
|
|
1950
|
+
* console.log(quota.storage.used, quota.storage.limit)
|
|
1878
1951
|
*/
|
|
1879
1952
|
async getQuota(path) {
|
|
1880
1953
|
path = path || 'INBOX';
|
|
@@ -1938,7 +2011,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1938
2011
|
* @property {String} path mailbox path
|
|
1939
2012
|
* @property {String} name mailbox name (last part of path after delimiter)
|
|
1940
2013
|
* @property {String} delimiter mailbox path delimiter, usually "." or "/"
|
|
1941
|
-
* @property {
|
|
2014
|
+
* @property {Set<string>} flags list of flags for this mailbox
|
|
1942
2015
|
* @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
2016
|
* @property {Boolean} listed `true` if mailbox was found from the output of LIST command
|
|
1944
2017
|
* @property {Boolean} subscribed `true` if mailbox was found from the output of LSUB command
|
|
@@ -2206,7 +2279,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2206
2279
|
* Sets flags for a message or message range
|
|
2207
2280
|
*
|
|
2208
2281
|
* @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
|
|
2282
|
+
* @param {string[]} flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
|
|
2210
2283
|
* @param {Object} [options]
|
|
2211
2284
|
* @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
|
|
2212
2285
|
* @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 +2313,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2240
2313
|
* Adds flags for a message or message range
|
|
2241
2314
|
*
|
|
2242
2315
|
* @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
|
|
2316
|
+
* @param {string[]} flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
|
|
2244
2317
|
* @param {Object} [options]
|
|
2245
2318
|
* @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
|
|
2246
2319
|
* @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 +2347,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2274
2347
|
* Remove specific flags from a message or message range
|
|
2275
2348
|
*
|
|
2276
2349
|
* @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
|
|
2350
|
+
* @param {string[]} flags Array of flags to remove. Only flags that are permitted to set are used, other flags are ignored
|
|
2278
2351
|
* @param {Object} [options]
|
|
2279
2352
|
* @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
|
|
2280
2353
|
* @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 +2381,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2308
2381
|
* Sets a colored flag for an email. Only supported by mail clients like Apple Mail
|
|
2309
2382
|
*
|
|
2310
2383
|
* @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'
|
|
2384
|
+
* @param {string} color The color to set. One of 'red', 'orange', 'yellow', 'green', 'blue', 'purple', and 'grey'
|
|
2312
2385
|
* @param {Object} [options]
|
|
2313
2386
|
* @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
|
|
2314
2387
|
* @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 +2598,13 @@ class ImapFlow extends EventEmitter {
|
|
|
2525
2598
|
* @property {boolean | Object} [source] if `true` then include full message in the response
|
|
2526
2599
|
* @property {Number} [source.start] include full message in the response starting from *start* byte
|
|
2527
2600
|
* @property {Number} [source.maxLength] include full message in the response, up to *maxLength* bytes
|
|
2528
|
-
* @property {
|
|
2601
|
+
* @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
2602
|
* @property {Boolean} [labels] if `true` then include GMail labels in the response (only if server supports `X-GM-EXT-1` extension)
|
|
2530
2603
|
* @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
2604
|
* @property {string[]} [bodyParts] An array of BODYPART identifiers to include in the response
|
|
2605
|
+
* @property {Boolean} [fast] IMAP macro equivalent to `flags`, `internalDate`, `size`
|
|
2606
|
+
* @property {Boolean} [all] IMAP macro equivalent to `flags`, `internalDate`, `size`, `envelope`
|
|
2607
|
+
* @property {Boolean} [full] IMAP macro equivalent to `flags`, `internalDate`, `size`, `envelope`, `bodyStructure`
|
|
2532
2608
|
*/
|
|
2533
2609
|
|
|
2534
2610
|
/**
|
|
@@ -2584,7 +2660,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2584
2660
|
* @property {Buffer} [source] message source for the requested byte range
|
|
2585
2661
|
* @property {BigInt} [modseq] message Modseq number. Always included if the server supports CONDSTORE extension
|
|
2586
2662
|
* @property {String} [emailId] unique email ID. Always included if server supports `OBJECTID` or `X-GM-EXT-1` extensions
|
|
2587
|
-
* @property {String} [
|
|
2663
|
+
* @property {String} [threadId] unique thread ID. Only present if server supports `OBJECTID` or `X-GM-EXT-1` extension
|
|
2588
2664
|
* @property {Set<string>} [labels] a Set of labels. Only present if server supports `X-GM-EXT-1` extension
|
|
2589
2665
|
* @property {Number} [size] message size
|
|
2590
2666
|
* @property {Set<string>} [flags] a set of message flags
|
|
@@ -2629,6 +2705,11 @@ class ImapFlow extends EventEmitter {
|
|
|
2629
2705
|
return false;
|
|
2630
2706
|
}
|
|
2631
2707
|
|
|
2708
|
+
// Push/pull coordination for the async generator pattern:
|
|
2709
|
+
// The FETCH command handler pushes results into rowQueue via onUntaggedFetch.
|
|
2710
|
+
// The generator consumer pulls via getNext(). The `push` callback bridges the
|
|
2711
|
+
// two: when the consumer is waiting and the queue is empty, `push` is set to
|
|
2712
|
+
// a function that wakes up the consumer when new data arrives.
|
|
2632
2713
|
let finished = false;
|
|
2633
2714
|
let push = false;
|
|
2634
2715
|
let rowQueue = [];
|
|
@@ -2649,7 +2730,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2649
2730
|
return resolve(null);
|
|
2650
2731
|
}
|
|
2651
2732
|
|
|
2652
|
-
//
|
|
2733
|
+
// No data available yet; register a wakeup callback
|
|
2653
2734
|
push = () => {
|
|
2654
2735
|
push = false;
|
|
2655
2736
|
check();
|
|
@@ -2658,6 +2739,10 @@ class ImapFlow extends EventEmitter {
|
|
|
2658
2739
|
check();
|
|
2659
2740
|
});
|
|
2660
2741
|
|
|
2742
|
+
// Fire-and-forget the FETCH command. It runs in the background while
|
|
2743
|
+
// the generator yields results. Each untagged FETCH response is paired
|
|
2744
|
+
// with a `next` callback that acts as backpressure: the FETCH handler
|
|
2745
|
+
// won't process the next response until the consumer calls next().
|
|
2661
2746
|
this.run('FETCH', range, query, {
|
|
2662
2747
|
uid: !!options.uid,
|
|
2663
2748
|
binary: options.binary,
|
|
@@ -2697,6 +2782,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2697
2782
|
|
|
2698
2783
|
if (res !== null) {
|
|
2699
2784
|
yield res.response;
|
|
2785
|
+
// Signal the FETCH handler to process the next untagged response
|
|
2700
2786
|
res.next();
|
|
2701
2787
|
}
|
|
2702
2788
|
}
|
|
@@ -2833,8 +2919,9 @@ class ImapFlow extends EventEmitter {
|
|
|
2833
2919
|
let uid = false;
|
|
2834
2920
|
|
|
2835
2921
|
if (part === '1') {
|
|
2836
|
-
//
|
|
2837
|
-
// the
|
|
2922
|
+
// Special handling for part "1": in single-node emails (no childNodes),
|
|
2923
|
+
// the body is accessed via "TEXT" rather than "1", and headers via
|
|
2924
|
+
// "HEADER" instead of "1.MIME". Check bodyStructure to detect this.
|
|
2838
2925
|
let response = await this.fetchOne(range, { uid: true, bodyStructure: true }, options);
|
|
2839
2926
|
|
|
2840
2927
|
if (!response) {
|
|
@@ -2994,6 +3081,13 @@ class ImapFlow extends EventEmitter {
|
|
|
2994
3081
|
let output;
|
|
2995
3082
|
let fetchAborted = false;
|
|
2996
3083
|
|
|
3084
|
+
// Build a decoder pipeline that progressively transforms the raw FETCH data:
|
|
3085
|
+
// 1. Transfer-encoding decoder (base64 or quoted-printable -> binary)
|
|
3086
|
+
// 2. Format decoder (format=flowed -> plain text, if applicable)
|
|
3087
|
+
// 3. Charset decoder (non-UTF-8 -> UTF-8, for text parts only)
|
|
3088
|
+
// 4. Byte limiter (enforces maxBytes cap)
|
|
3089
|
+
// `stream` is the head of the pipeline (where raw chunks are written),
|
|
3090
|
+
// `output` is the tail (what the caller reads from).
|
|
2997
3091
|
switch (meta.encoding) {
|
|
2998
3092
|
case 'base64':
|
|
2999
3093
|
output = stream = new libbase64.Decoder();
|
|
@@ -3007,7 +3101,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3007
3101
|
|
|
3008
3102
|
let isTextNode = ['text/html', 'text/plain', 'text/x-amp-html'].includes(meta.contentType) || (part === '1' && !meta.contentType);
|
|
3009
3103
|
if ((!meta.disposition || meta.disposition === 'inline') && isTextNode) {
|
|
3010
|
-
// flowed text
|
|
3104
|
+
// RFC 3676 format=flowed text: unwrap soft line breaks
|
|
3011
3105
|
if (meta.flowed) {
|
|
3012
3106
|
let flowDecoder = new FlowedDecoder({
|
|
3013
3107
|
delSp: meta.delSp
|
|
@@ -3018,7 +3112,8 @@ class ImapFlow extends EventEmitter {
|
|
|
3018
3112
|
output = output.pipe(flowDecoder);
|
|
3019
3113
|
}
|
|
3020
3114
|
|
|
3021
|
-
//
|
|
3115
|
+
// Convert non-UTF-8 charsets to UTF-8 via a streaming decoder.
|
|
3116
|
+
// ASCII and UTF-8 need no conversion. Unknown charsets are left as-is.
|
|
3022
3117
|
if (meta.charset && !['ascii', 'usascii', 'utf8'].includes(meta.charset.toLowerCase().replace(/[^a-z0-9]+/g, ''))) {
|
|
3023
3118
|
try {
|
|
3024
3119
|
let decoder = getDecoder(meta.charset);
|
|
@@ -3059,6 +3154,9 @@ class ImapFlow extends EventEmitter {
|
|
|
3059
3154
|
return stream.write(chunk);
|
|
3060
3155
|
};
|
|
3061
3156
|
|
|
3157
|
+
// Fetch remaining chunks in a loop, writing each to the decoder stream.
|
|
3158
|
+
// Stops when the server returns a short chunk (< chunkSize), the byte
|
|
3159
|
+
// limiter is satisfied, or the consumer destroys the output stream.
|
|
3062
3160
|
let fetchAllParts = async () => {
|
|
3063
3161
|
while (hasMore && !limiter.limited && !fetchAborted) {
|
|
3064
3162
|
let { chunk } = await getNextPart();
|
|
@@ -3108,6 +3206,11 @@ class ImapFlow extends EventEmitter {
|
|
|
3108
3206
|
}
|
|
3109
3207
|
};
|
|
3110
3208
|
|
|
3209
|
+
// Kick off the download pipeline asynchronously. The first chunk was
|
|
3210
|
+
// already fetched above (to get metadata); write it to the decoder
|
|
3211
|
+
// stream and then fetch remaining chunks via fetchAllParts().
|
|
3212
|
+
// setImmediate ensures the caller gets the {meta, content} return
|
|
3213
|
+
// value before streaming begins.
|
|
3111
3214
|
setImmediate(() => {
|
|
3112
3215
|
let writeResult;
|
|
3113
3216
|
try {
|
|
@@ -3180,7 +3283,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3180
3283
|
* Fetch multiple attachments as Buffer values
|
|
3181
3284
|
*
|
|
3182
3285
|
* @param {SequenceString} range UID or sequence number for the message to fetch
|
|
3183
|
-
* @param {String} parts A list of bodystructure parts
|
|
3286
|
+
* @param {String[]} parts A list of bodystructure parts
|
|
3184
3287
|
* @param {Object} [options]
|
|
3185
3288
|
* @param {Boolean} [options.uid] If `true` then uses UID number instead of sequence number for `range`
|
|
3186
3289
|
* @returns {Promise<Object>} Download data object
|
|
@@ -3337,11 +3440,14 @@ class ImapFlow extends EventEmitter {
|
|
|
3337
3440
|
return result;
|
|
3338
3441
|
}
|
|
3339
3442
|
|
|
3443
|
+
// Mailbox lock queue processor. Implements a mutex pattern: only one lock
|
|
3444
|
+
// is active at a time. When the active lock is released, the next queued
|
|
3445
|
+
// lock is processed. The `processingLock` flag prevents concurrent runs
|
|
3446
|
+
// of this method (which could happen via setImmediate re-entry from release()).
|
|
3340
3447
|
async processLocks() {
|
|
3341
|
-
// Atomic test-and-set to prevent race condition
|
|
3342
3448
|
const wasProcessing = this.processingLock;
|
|
3343
3449
|
if (wasProcessing) {
|
|
3344
|
-
// Another processor is already running
|
|
3450
|
+
// Another processor is already running; it will pick up new locks
|
|
3345
3451
|
this.log.trace({
|
|
3346
3452
|
msg: 'Mailbox locking queued',
|
|
3347
3453
|
path: this.mailbox && this.mailbox.path,
|
|
@@ -3405,7 +3511,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3405
3511
|
}
|
|
3406
3512
|
|
|
3407
3513
|
if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
|
|
3408
|
-
//
|
|
3514
|
+
// Fast path: mailbox is already selected with the right access mode
|
|
3409
3515
|
this.log.trace({
|
|
3410
3516
|
msg: 'Mailbox lock acquired [existing]',
|
|
3411
3517
|
path,
|
|
@@ -3415,10 +3521,10 @@ class ImapFlow extends EventEmitter {
|
|
|
3415
3521
|
});
|
|
3416
3522
|
this.currentLock = lock;
|
|
3417
3523
|
resolve({ path, release });
|
|
3418
|
-
break; //
|
|
3524
|
+
break; // Stop processing; next lock waits for release()
|
|
3419
3525
|
} else {
|
|
3420
3526
|
try {
|
|
3421
|
-
//
|
|
3527
|
+
// Need to SELECT/EXAMINE a different mailbox
|
|
3422
3528
|
await this.mailboxOpen(path, options);
|
|
3423
3529
|
this.log.trace({
|
|
3424
3530
|
msg: 'Mailbox lock acquired [selected]',
|
|
@@ -3432,6 +3538,9 @@ class ImapFlow extends EventEmitter {
|
|
|
3432
3538
|
break; // Wait for this lock to be released
|
|
3433
3539
|
} catch (err) {
|
|
3434
3540
|
if (err.responseStatus === 'NO') {
|
|
3541
|
+
// SELECT failed with NO -- verify whether the mailbox exists
|
|
3542
|
+
// at all by running LIST. This sets mailboxMissing on the error
|
|
3543
|
+
// so the caller can distinguish "doesn't exist" from other failures.
|
|
3435
3544
|
try {
|
|
3436
3545
|
let folders = await this.run('LIST', '', path, { listOnly: true });
|
|
3437
3546
|
if (!folders || !folders.length) {
|
|
@@ -3458,9 +3567,10 @@ class ImapFlow extends EventEmitter {
|
|
|
3458
3567
|
} finally {
|
|
3459
3568
|
this.processingLock = false;
|
|
3460
3569
|
|
|
3461
|
-
//
|
|
3570
|
+
// New locks may have been queued while we were processing (e.g.,
|
|
3571
|
+
// a lock that failed immediately and the next getMailboxLock call
|
|
3572
|
+
// arrived before we finished). Schedule another run if needed.
|
|
3462
3573
|
if (this.locks.length && !this.currentLock) {
|
|
3463
|
-
// Process any remaining locks
|
|
3464
3574
|
setImmediate(() => {
|
|
3465
3575
|
this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
|
|
3466
3576
|
});
|
|
@@ -3561,6 +3671,14 @@ class ImapFlow extends EventEmitter {
|
|
|
3561
3671
|
return synteticLogger;
|
|
3562
3672
|
}
|
|
3563
3673
|
|
|
3674
|
+
/**
|
|
3675
|
+
* Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
|
|
3676
|
+
* (e.g., STARTTLS) or transferring socket ownership.
|
|
3677
|
+
*
|
|
3678
|
+
* @returns {Object} Socket objects
|
|
3679
|
+
* @returns {Object} return.readSocket The read socket (inflated socket if compression is enabled, raw socket otherwise)
|
|
3680
|
+
* @returns {Object} return.writeSocket The write socket
|
|
3681
|
+
*/
|
|
3564
3682
|
unbind() {
|
|
3565
3683
|
this.socket.unpipe(this.streamer);
|
|
3566
3684
|
if (this._inflate) {
|
|
@@ -3620,6 +3738,9 @@ class ImapFlow extends EventEmitter {
|
|
|
3620
3738
|
* @type {Object}
|
|
3621
3739
|
* @property {String} path mailbox path this event applies to
|
|
3622
3740
|
* @property {Number} seq sequence number of deleted message
|
|
3741
|
+
* @property {Boolean} vanished `true` if message was expunged via VANISHED response
|
|
3742
|
+
* @property {Number} [uid] UID of expunged message (when `vanished` is `true`)
|
|
3743
|
+
* @property {Boolean} [earlier] `true` for VANISHED EARLIER responses
|
|
3623
3744
|
* @example
|
|
3624
3745
|
* client.on('expunge', data=>{
|
|
3625
3746
|
* console.log(`Message #${data.seq} was deleted from "${data.path}"`);
|
|
@@ -3636,6 +3757,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3636
3757
|
* @property {Number} [uid] UID number of updated message (if server provided this value)
|
|
3637
3758
|
* @property {BigInt} [modseq] Updated modseq number for the mailbox (if server provided this value)
|
|
3638
3759
|
* @property {Set<string>} flags A set of all flags for the updated message
|
|
3760
|
+
* @property {String} [flagColor] flag color like "red", or "yellow". Derived from the `flags` Set using Apple Mail color rules
|
|
3639
3761
|
* @example
|
|
3640
3762
|
* client.on('flags', data=>{
|
|
3641
3763
|
* console.log(`Flag set for #${data.seq} is now "${Array.from(data.flags).join(', ')}"`);
|
|
@@ -3671,7 +3793,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3671
3793
|
* @type {Object}
|
|
3672
3794
|
* @example
|
|
3673
3795
|
* client.on('log', entry => {
|
|
3674
|
-
* console.log(`${
|
|
3796
|
+
* console.log(`${entry.cid} ${entry.msg}`);
|
|
3675
3797
|
* });
|
|
3676
3798
|
*/
|
|
3677
3799
|
|
package/lib/jp-decoder.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
const { Transform } = require('stream');
|
|
4
4
|
const encodingJapanese = require('encoding-japanese');
|
|
5
5
|
|
|
6
|
+
// A Transform stream for decoding Japanese character sets (Shift_JIS, EUC-JP, ISO-2022-JP).
|
|
7
|
+
// Unlike iconv-lite which can decode incrementally, encoding-japanese requires the complete
|
|
8
|
+
// input buffer for accurate charset detection and stateful decoding (especially ISO-2022-JP
|
|
9
|
+
// which uses escape sequences to switch between ASCII and multi-byte modes). Therefore,
|
|
10
|
+
// this stream buffers all input during _transform and performs the actual decoding in _flush.
|
|
6
11
|
class JPDecoder extends Transform {
|
|
7
12
|
constructor(charset) {
|
|
8
13
|
super();
|
|
@@ -12,6 +17,8 @@ class JPDecoder extends Transform {
|
|
|
12
17
|
this.chunklen = 0;
|
|
13
18
|
}
|
|
14
19
|
|
|
20
|
+
// Buffer all incoming chunks; no decoding happens here because Japanese charsets
|
|
21
|
+
// require the complete input for accurate conversion.
|
|
15
22
|
_transform(chunk, encoding, done) {
|
|
16
23
|
if (typeof chunk === 'string') {
|
|
17
24
|
chunk = Buffer.from(chunk, encoding);
|
|
@@ -22,6 +29,9 @@ class JPDecoder extends Transform {
|
|
|
22
29
|
done();
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
// Perform the actual charset conversion once all input has been received.
|
|
33
|
+
// Uses the encoding-japanese library to convert from the source charset to Unicode.
|
|
34
|
+
// On failure (corrupt or unrecognizable data), passes through the raw bytes unchanged.
|
|
25
35
|
_flush(done) {
|
|
26
36
|
let input = Buffer.concat(this.chunks, this.chunklen);
|
|
27
37
|
try {
|