imapflow 1.2.1 → 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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/lib/imap-flow.js +28 -7
- package/package.json +1 -1
- package/test/connection-edge-cases-test.js +246 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## [1.2.1](https://github.com/postalsys/imapflow/compare/v1.2.0...v1.2.1) (2025-12-17)
|
|
4
11
|
|
|
5
12
|
|
package/lib/imap-flow.js
CHANGED
|
@@ -1730,18 +1730,39 @@ class ImapFlow extends EventEmitter {
|
|
|
1730
1730
|
}
|
|
1731
1731
|
}
|
|
1732
1732
|
|
|
1733
|
-
//
|
|
1734
|
-
|
|
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
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -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
|
+
};
|