imapflow 1.2.1 → 1.2.3

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.1"
2
+ ".": "1.2.3"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.3](https://github.com/postalsys/imapflow/compare/v1.2.2...v1.2.3) (2025-12-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * Add logging for silently swallowed download errors ([b9f1012](https://github.com/postalsys/imapflow/commit/b9f101249236f55c48a68fd6f320d550055dd4e4))
9
+
10
+ ## [1.2.2](https://github.com/postalsys/imapflow/compare/v1.2.1...v1.2.2) (2025-12-22)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * Reject pending mailbox locks on connection close ([d20f060](https://github.com/postalsys/imapflow/commit/d20f060faafe5bf4740bdb067008d1d8e7b31592))
16
+
3
17
  ## [1.2.1](https://github.com/postalsys/imapflow/compare/v1.2.0...v1.2.1) (2025-12-17)
4
18
 
5
19
 
package/lib/imap-flow.js CHANGED
@@ -1730,18 +1730,39 @@ class ImapFlow extends EventEmitter {
1730
1730
  }
1731
1731
  }
1732
1732
 
1733
- // Defer rejections to next tick to allow event handlers to be properly set up
1734
- // This helps prevent unhandled rejections when connection closes unexpectedly
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)
1735
1745
  if (pendingRequests.length) {
1736
1746
  let byeReason = this.byeReason;
1737
1747
  setImmediate(() => {
1738
1748
  for (let request of pendingRequests) {
1739
- const error = new Error('Connection not available');
1740
- error.code = 'NoConnection';
1741
- if (byeReason) {
1742
- error.reason = byeReason;
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));
1743
1765
  }
1744
- request.reject(error);
1745
1766
  }
1746
1767
  });
1747
1768
  }
@@ -3079,6 +3100,15 @@ class ImapFlow extends EventEmitter {
3079
3100
  .catch(err => {
3080
3101
  if (!fetchAborted && stream && !stream.destroyed) {
3081
3102
  stream.emit('error', err);
3103
+ } else {
3104
+ // Log when error cannot be emitted to stream
3105
+ this.log.warn({
3106
+ msg: 'Download error after stream closed',
3107
+ err,
3108
+ fetchAborted,
3109
+ streamDestroyed: stream?.destroyed,
3110
+ cid: this.id
3111
+ });
3082
3112
  }
3083
3113
  })
3084
3114
  .finally(() => {
@@ -3093,6 +3123,15 @@ class ImapFlow extends EventEmitter {
3093
3123
  .catch(err => {
3094
3124
  if (!fetchAborted && stream && !stream.destroyed) {
3095
3125
  stream.emit('error', err);
3126
+ } else {
3127
+ // Log when error cannot be emitted to stream
3128
+ this.log.warn({
3129
+ msg: 'Download error after stream closed',
3130
+ err,
3131
+ fetchAborted,
3132
+ streamDestroyed: stream?.destroyed,
3133
+ cid: this.id
3134
+ });
3096
3135
  }
3097
3136
  })
3098
3137
  .finally(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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
+ };