imapflow 1.2.0 → 1.2.2

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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.2.0"
2
+ ".": "1.2.2"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.2](https://github.com/postalsys/imapflow/compare/v1.2.1...v1.2.2) (2025-12-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * Reject pending mailbox locks on connection close ([d20f060](https://github.com/postalsys/imapflow/commit/d20f060faafe5bf4740bdb067008d1d8e7b31592))
9
+
10
+ ## [1.2.1](https://github.com/postalsys/imapflow/compare/v1.2.0...v1.2.1) (2025-12-17)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * Defer promise rejections on connection close to prevent unhandled rejections ([e53d9a8](https://github.com/postalsys/imapflow/commit/e53d9a8e0028197a5ff3c7cda342e0c11b64f228))
16
+
3
17
  ## [1.2.0](https://github.com/postalsys/imapflow/compare/v1.1.1...v1.2.0) (2025-12-11)
4
18
 
5
19
 
package/lib/imap-flow.js CHANGED
@@ -1252,7 +1252,17 @@ class ImapFlow extends EventEmitter {
1252
1252
  });
1253
1253
  }
1254
1254
 
1255
- async serverBye() {
1255
+ async serverBye(parsed) {
1256
+ // Extract BYE reason from response for better error messages
1257
+ let reason =
1258
+ parsed &&
1259
+ parsed.attributes &&
1260
+ parsed.attributes
1261
+ .filter(val => val.type === 'TEXT')
1262
+ .map(val => val.value.trim())
1263
+ .join(' ');
1264
+
1265
+ this.byeReason = reason || 'Server closed connection';
1256
1266
  this.untaggedHandlers.BYE = null;
1257
1267
  this.state = this.states.LOGOUT;
1258
1268
  }
@@ -1695,14 +1705,15 @@ class ImapFlow extends EventEmitter {
1695
1705
  this.preCheck().catch(err => this.log.warn({ err, cid: this.id }));
1696
1706
  }
1697
1707
 
1708
+ // Collect all pending requests to reject
1709
+ let pendingRequests = [];
1710
+
1698
1711
  // reject command that is currently processed
1699
1712
  if (this.currentRequest && this.requestTagMap.has(this.currentRequest.tag)) {
1700
1713
  let request = this.requestTagMap.get(this.currentRequest.tag);
1701
1714
  if (request) {
1702
1715
  this.requestTagMap.delete(request.tag);
1703
- const error = new Error('Connection not available');
1704
- error.code = 'NoConnection';
1705
- request.reject(error);
1716
+ pendingRequests.push(request);
1706
1717
  }
1707
1718
  this.currentRequest = false;
1708
1719
  }
@@ -1714,13 +1725,48 @@ class ImapFlow extends EventEmitter {
1714
1725
  let request = this.requestTagMap.get(req.tag);
1715
1726
  if (request) {
1716
1727
  this.requestTagMap.delete(request.tag);
1717
- const error = new Error('Connection not available');
1718
- error.code = 'NoConnection';
1719
- request.reject(error);
1728
+ pendingRequests.push(request);
1720
1729
  }
1721
1730
  }
1722
1731
  }
1723
1732
 
1733
+ // Helper to create connection error
1734
+ const createNoConnectionError = byeReason => {
1735
+ const error = new Error('Connection not available');
1736
+ error.code = 'NoConnection';
1737
+ if (byeReason) {
1738
+ error.reason = byeReason;
1739
+ }
1740
+ return error;
1741
+ };
1742
+
1743
+ // Reject pending requests via setImmediate to ensure caller's promise chain
1744
+ // is fully set up before rejection (prevents unhandled promise rejections)
1745
+ if (pendingRequests.length) {
1746
+ let byeReason = this.byeReason;
1747
+ setImmediate(() => {
1748
+ for (let request of pendingRequests) {
1749
+ request.reject(createNoConnectionError(byeReason));
1750
+ }
1751
+ });
1752
+ }
1753
+
1754
+ // Clear current lock - holder will see errors when they try operations
1755
+ this.currentLock = false;
1756
+
1757
+ // Reject pending mailbox locks via setImmediate for consistency
1758
+ if (this.locks && this.locks.length) {
1759
+ let byeReason = this.byeReason;
1760
+ let pendingLocks = this.locks.splice(0); // Take all locks and clear the array
1761
+ setImmediate(() => {
1762
+ for (let lock of pendingLocks) {
1763
+ if (typeof lock.reject === 'function') {
1764
+ lock.reject(createNoConnectionError(byeReason));
1765
+ }
1766
+ }
1767
+ });
1768
+ }
1769
+
1724
1770
  // cleanup compression streams if they exist
1725
1771
  if (this._inflate) {
1726
1772
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "lib/imap-flow.js",
6
6
  "types": "lib/imap-flow.d.ts",
@@ -700,3 +700,249 @@ module.exports['Connection Edge: Event handlers attached before piping'] = test
700
700
 
701
701
  test.done();
702
702
  };
703
+
704
+ module.exports['Connection Edge: Pending locks rejected on close'] = test => {
705
+ let client = new ImapFlow({
706
+ host: 'imap.example.com',
707
+ port: 993,
708
+ auth: { user: 'test', pass: 'test' }
709
+ });
710
+
711
+ // Initialize locks array with pending locks
712
+ let rejectedErrors = [];
713
+ client.locks = [
714
+ {
715
+ path: 'INBOX',
716
+ lockId: 'lock1',
717
+ resolve: () => {},
718
+ reject: err => rejectedErrors.push(err)
719
+ },
720
+ {
721
+ path: 'Sent',
722
+ lockId: 'lock2',
723
+ resolve: () => {},
724
+ reject: err => rejectedErrors.push(err)
725
+ }
726
+ ];
727
+
728
+ // Mock socket to allow close() to run
729
+ client.socket = { destroyed: false, destroy: () => {} };
730
+ client.usable = true;
731
+
732
+ // Call close
733
+ client.close();
734
+
735
+ // Rejections happen via setImmediate, so check after next tick
736
+ setImmediate(() => {
737
+ test.equal(rejectedErrors.length, 2, 'All pending locks should be rejected');
738
+ test.equal(rejectedErrors[0].code, 'NoConnection', 'Error should have NoConnection code');
739
+ test.equal(rejectedErrors[1].code, 'NoConnection', 'Error should have NoConnection code');
740
+ test.equal(client.locks.length, 0, 'Locks array should be cleared');
741
+ test.done();
742
+ });
743
+ };
744
+
745
+ module.exports['Connection Edge: Lock rejection includes byeReason'] = test => {
746
+ let client = new ImapFlow({
747
+ host: 'imap.example.com',
748
+ port: 993,
749
+ auth: { user: 'test', pass: 'test' }
750
+ });
751
+
752
+ // Set byeReason before close
753
+ client.byeReason = 'Server shutting down';
754
+
755
+ let rejectedError = null;
756
+ client.locks = [
757
+ {
758
+ path: 'INBOX',
759
+ lockId: 'lock1',
760
+ resolve: () => {},
761
+ reject: err => {
762
+ rejectedError = err;
763
+ }
764
+ }
765
+ ];
766
+
767
+ client.socket = { destroyed: false, destroy: () => {} };
768
+ client.usable = true;
769
+
770
+ client.close();
771
+
772
+ setImmediate(() => {
773
+ test.ok(rejectedError, 'Lock should be rejected');
774
+ test.equal(rejectedError.code, 'NoConnection');
775
+ test.equal(rejectedError.reason, 'Server shutting down', 'byeReason should be included');
776
+ test.done();
777
+ });
778
+ };
779
+
780
+ module.exports['Connection Edge: currentLock cleared on close'] = test => {
781
+ let client = new ImapFlow({
782
+ host: 'imap.example.com',
783
+ port: 993,
784
+ auth: { user: 'test', pass: 'test' }
785
+ });
786
+
787
+ // Simulate an active lock
788
+ client.currentLock = {
789
+ path: 'INBOX',
790
+ lockId: 'active-lock',
791
+ release: () => {}
792
+ };
793
+ client.locks = [];
794
+
795
+ client.socket = { destroyed: false, destroy: () => {} };
796
+ client.usable = true;
797
+
798
+ client.close();
799
+
800
+ test.equal(client.currentLock, false, 'currentLock should be cleared');
801
+ test.done();
802
+ };
803
+
804
+ module.exports['Connection Edge: Lock rejection handles missing reject function'] = test => {
805
+ let client = new ImapFlow({
806
+ host: 'imap.example.com',
807
+ port: 993,
808
+ auth: { user: 'test', pass: 'test' }
809
+ });
810
+
811
+ // Create lock with missing reject function (edge case)
812
+ let validRejected = false;
813
+ client.locks = [
814
+ {
815
+ path: 'INBOX',
816
+ lockId: 'lock1',
817
+ resolve: () => {}
818
+ // No reject function
819
+ },
820
+ {
821
+ path: 'Sent',
822
+ lockId: 'lock2',
823
+ resolve: () => {},
824
+ reject: () => {
825
+ validRejected = true;
826
+ }
827
+ }
828
+ ];
829
+
830
+ client.socket = { destroyed: false, destroy: () => {} };
831
+ client.usable = true;
832
+
833
+ // Should not throw
834
+ test.doesNotThrow(() => {
835
+ client.close();
836
+ }, 'Should handle missing reject function gracefully');
837
+
838
+ setImmediate(() => {
839
+ test.ok(validRejected, 'Valid lock should still be rejected');
840
+ test.equal(client.locks.length, 0, 'Locks array should be cleared');
841
+ test.done();
842
+ });
843
+ };
844
+
845
+ module.exports['Connection Edge: Close with empty locks array'] = test => {
846
+ let client = new ImapFlow({
847
+ host: 'imap.example.com',
848
+ port: 993,
849
+ auth: { user: 'test', pass: 'test' }
850
+ });
851
+
852
+ client.locks = [];
853
+ client.currentLock = false;
854
+ client.socket = { destroyed: false, destroy: () => {} };
855
+ client.usable = true;
856
+
857
+ // Should not throw with empty locks
858
+ test.doesNotThrow(() => {
859
+ client.close();
860
+ }, 'Should handle empty locks array');
861
+
862
+ test.equal(client.currentLock, false);
863
+ test.done();
864
+ };
865
+
866
+ module.exports['Connection Edge: Lock rejection is deferred via setImmediate'] = test => {
867
+ let client = new ImapFlow({
868
+ host: 'imap.example.com',
869
+ port: 993,
870
+ auth: { user: 'test', pass: 'test' }
871
+ });
872
+
873
+ let rejectionTime = null;
874
+ let closeTime = null;
875
+
876
+ client.locks = [
877
+ {
878
+ path: 'INBOX',
879
+ lockId: 'lock1',
880
+ resolve: () => {},
881
+ reject: () => {
882
+ rejectionTime = Date.now();
883
+ }
884
+ }
885
+ ];
886
+
887
+ client.socket = { destroyed: false, destroy: () => {} };
888
+ client.usable = true;
889
+
890
+ client.close();
891
+ closeTime = Date.now();
892
+
893
+ // Rejection should not have happened yet (synchronously)
894
+ test.equal(rejectionTime, null, 'Rejection should be deferred');
895
+
896
+ setImmediate(() => {
897
+ test.ok(rejectionTime !== null, 'Rejection should happen after setImmediate');
898
+ test.ok(rejectionTime >= closeTime, 'Rejection should happen after close()');
899
+ test.done();
900
+ });
901
+ };
902
+
903
+ module.exports['Connection Edge: Pending requests and locks both rejected on close'] = test => {
904
+ let client = new ImapFlow({
905
+ host: 'imap.example.com',
906
+ port: 993,
907
+ auth: { user: 'test', pass: 'test' }
908
+ });
909
+
910
+ let requestRejected = false;
911
+ let lockRejected = false;
912
+
913
+ // Add pending request - must be in requestQueue and requestTagMap
914
+ let request = {
915
+ tag: 'A001',
916
+ reject: err => {
917
+ requestRejected = true;
918
+ test.equal(err.code, 'NoConnection');
919
+ }
920
+ };
921
+ client.requestTagMap = new Map();
922
+ client.requestTagMap.set('A001', request);
923
+ client.requestQueue = [request];
924
+
925
+ // Add pending lock
926
+ client.locks = [
927
+ {
928
+ path: 'INBOX',
929
+ lockId: 'lock1',
930
+ resolve: () => {},
931
+ reject: err => {
932
+ lockRejected = true;
933
+ test.equal(err.code, 'NoConnection');
934
+ }
935
+ }
936
+ ];
937
+
938
+ client.socket = { destroyed: false, destroy: () => {} };
939
+ client.usable = true;
940
+
941
+ client.close();
942
+
943
+ setImmediate(() => {
944
+ test.ok(requestRejected, 'Pending request should be rejected');
945
+ test.ok(lockRejected, 'Pending lock should be rejected');
946
+ test.done();
947
+ });
948
+ };