imapflow 1.2.2 → 1.2.4
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 +14 -0
- package/lib/imap-flow.js +41 -0
- package/package.json +6 -6
- package/test/memory-cleanup-test.js +144 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.4](https://github.com/postalsys/imapflow/compare/v1.2.3...v1.2.4) (2025-12-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* Improve resource cleanup on connection close ([7c3244c](https://github.com/postalsys/imapflow/commit/7c3244cd2d6c7c3d7be54ba26ad2ce5d8b9ddae9))
|
|
9
|
+
|
|
10
|
+
## [1.2.3](https://github.com/postalsys/imapflow/compare/v1.2.2...v1.2.3) (2025-12-22)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* Add logging for silently swallowed download errors ([b9f1012](https://github.com/postalsys/imapflow/commit/b9f101249236f55c48a68fd6f320d550055dd4e4))
|
|
16
|
+
|
|
3
17
|
## [1.2.2](https://github.com/postalsys/imapflow/compare/v1.2.1...v1.2.2) (2025-12-22)
|
|
4
18
|
|
|
5
19
|
|
package/lib/imap-flow.js
CHANGED
|
@@ -1788,6 +1788,29 @@ class ImapFlow extends EventEmitter {
|
|
|
1788
1788
|
}
|
|
1789
1789
|
}
|
|
1790
1790
|
|
|
1791
|
+
// cleanup streamer
|
|
1792
|
+
if (this.streamer) {
|
|
1793
|
+
try {
|
|
1794
|
+
// remove our listeners
|
|
1795
|
+
if (this.socketReadable) {
|
|
1796
|
+
this.streamer.removeListener('readable', this.socketReadable);
|
|
1797
|
+
}
|
|
1798
|
+
this.streamer.removeAllListeners('error');
|
|
1799
|
+
if (!this.streamer.destroyed) {
|
|
1800
|
+
this.streamer.destroy();
|
|
1801
|
+
}
|
|
1802
|
+
} catch (err) {
|
|
1803
|
+
this.log.error({ err, info: 'Failed to cleanup streamer', cid: this.id });
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// clear socket handlers
|
|
1808
|
+
this.clearSocketHandlers();
|
|
1809
|
+
|
|
1810
|
+
// clear cached data
|
|
1811
|
+
this.folders.clear();
|
|
1812
|
+
this.requestTagMap.clear();
|
|
1813
|
+
|
|
1791
1814
|
this.state = this.states.LOGOUT;
|
|
1792
1815
|
if (this.isClosed) {
|
|
1793
1816
|
return;
|
|
@@ -3100,6 +3123,15 @@ class ImapFlow extends EventEmitter {
|
|
|
3100
3123
|
.catch(err => {
|
|
3101
3124
|
if (!fetchAborted && stream && !stream.destroyed) {
|
|
3102
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
|
+
});
|
|
3103
3135
|
}
|
|
3104
3136
|
})
|
|
3105
3137
|
.finally(() => {
|
|
@@ -3114,6 +3146,15 @@ class ImapFlow extends EventEmitter {
|
|
|
3114
3146
|
.catch(err => {
|
|
3115
3147
|
if (!fetchAborted && stream && !stream.destroyed) {
|
|
3116
3148
|
stream.emit('error', err);
|
|
3149
|
+
} else {
|
|
3150
|
+
// Log when error cannot be emitted to stream
|
|
3151
|
+
this.log.warn({
|
|
3152
|
+
msg: 'Download error after stream closed',
|
|
3153
|
+
err,
|
|
3154
|
+
fetchAborted,
|
|
3155
|
+
streamDestroyed: stream?.destroyed,
|
|
3156
|
+
cid: this.id
|
|
3157
|
+
});
|
|
3117
3158
|
}
|
|
3118
3159
|
})
|
|
3119
3160
|
.finally(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "lib/imap-flow.js",
|
|
6
6
|
"types": "lib/imap-flow.d.ts",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"@babel/plugin-syntax-class-properties": "7.12.13",
|
|
36
36
|
"@babel/preset-env": "7.28.5",
|
|
37
37
|
"@eslint/eslintrc": "3.3.3",
|
|
38
|
-
"@eslint/js": "9.39.
|
|
39
|
-
"@types/node": "25.0.
|
|
40
|
-
"eslint": "9.39.
|
|
38
|
+
"@eslint/js": "9.39.2",
|
|
39
|
+
"@types/node": "25.0.3",
|
|
40
|
+
"eslint": "9.39.2",
|
|
41
41
|
"eslint-config-nodemailer": "1.2.0",
|
|
42
42
|
"eslint-config-prettier": "10.1.8",
|
|
43
43
|
"grunt": "1.6.1",
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"encoding-japanese": "2.2.0",
|
|
55
|
-
"iconv-lite": "0.7.
|
|
55
|
+
"iconv-lite": "0.7.1",
|
|
56
56
|
"libbase64": "1.3.0",
|
|
57
57
|
"libmime": "5.3.7",
|
|
58
58
|
"libqp": "2.1.1",
|
|
59
59
|
"@zone-eu/mailsplit": "5.4.8",
|
|
60
|
-
"nodemailer": "7.0.
|
|
60
|
+
"nodemailer": "7.0.12",
|
|
61
61
|
"pino": "10.1.0",
|
|
62
62
|
"socks": "2.8.7"
|
|
63
63
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tests for memory cleanup on connection close
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { ImapFlow } = require('../lib/imap-flow');
|
|
8
|
+
|
|
9
|
+
exports['Memory Cleanup Tests'] = {
|
|
10
|
+
'should clean up streamer on close without connection'(test) {
|
|
11
|
+
const client = new ImapFlow({
|
|
12
|
+
host: '127.0.0.1',
|
|
13
|
+
port: 1,
|
|
14
|
+
secure: false,
|
|
15
|
+
logger: false
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Check initial state
|
|
19
|
+
test.ok(client.streamer, 'streamer should exist');
|
|
20
|
+
test.ok(!client.streamer.destroyed, 'streamer should not be destroyed initially');
|
|
21
|
+
|
|
22
|
+
// Close without connecting
|
|
23
|
+
client.close();
|
|
24
|
+
|
|
25
|
+
// Verify cleanup
|
|
26
|
+
test.ok(client.streamer.destroyed, 'streamer should be destroyed after close');
|
|
27
|
+
test.equal(client.streamer.listenerCount('error'), 0, 'error listeners should be removed');
|
|
28
|
+
test.equal(client.folders.size, 0, 'folders should be cleared');
|
|
29
|
+
test.equal(client.requestTagMap.size, 0, 'requestTagMap should be cleared');
|
|
30
|
+
test.ok(client.isClosed, 'client should be marked as closed');
|
|
31
|
+
|
|
32
|
+
test.done();
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
'should remove event listeners on close'(test) {
|
|
36
|
+
const client = new ImapFlow({
|
|
37
|
+
host: '127.0.0.1',
|
|
38
|
+
port: 1,
|
|
39
|
+
secure: false,
|
|
40
|
+
logger: false
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Add a readable listener as if connect was called
|
|
44
|
+
client.socketReadable = () => {};
|
|
45
|
+
client.streamer.on('readable', client.socketReadable);
|
|
46
|
+
|
|
47
|
+
// Verify listener was added
|
|
48
|
+
test.equal(client.streamer.listenerCount('readable'), 1, 'readable listener should be present');
|
|
49
|
+
|
|
50
|
+
client.close();
|
|
51
|
+
|
|
52
|
+
// Check listeners after close
|
|
53
|
+
test.equal(client.streamer.listenerCount('readable'), 0, 'readable listener should be removed');
|
|
54
|
+
test.equal(client.streamer.listenerCount('error'), 0, 'error listeners should be removed');
|
|
55
|
+
|
|
56
|
+
test.done();
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
'should clear internal structures on close'(test) {
|
|
60
|
+
const client = new ImapFlow({
|
|
61
|
+
host: '127.0.0.1',
|
|
62
|
+
port: 1,
|
|
63
|
+
secure: false,
|
|
64
|
+
logger: false
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Add some data to internal structures
|
|
68
|
+
client.folders.set('INBOX', { path: 'INBOX' });
|
|
69
|
+
client.folders.set('Sent', { path: 'Sent' });
|
|
70
|
+
client.requestTagMap.set('A001', { tag: 'A001' });
|
|
71
|
+
client.requestTagMap.set('A002', { tag: 'A002' });
|
|
72
|
+
|
|
73
|
+
test.equal(client.folders.size, 2, 'folders should have entries');
|
|
74
|
+
test.equal(client.requestTagMap.size, 2, 'requestTagMap should have entries');
|
|
75
|
+
|
|
76
|
+
client.close();
|
|
77
|
+
|
|
78
|
+
test.equal(client.folders.size, 0, 'folders should be cleared after close');
|
|
79
|
+
test.equal(client.requestTagMap.size, 0, 'requestTagMap should be cleared after close');
|
|
80
|
+
|
|
81
|
+
test.done();
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
'should handle multiple close calls gracefully'(test) {
|
|
85
|
+
const client = new ImapFlow({
|
|
86
|
+
host: '127.0.0.1',
|
|
87
|
+
port: 1,
|
|
88
|
+
secure: false,
|
|
89
|
+
logger: false
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Call close multiple times
|
|
93
|
+
test.doesNotThrow(() => {
|
|
94
|
+
client.close();
|
|
95
|
+
client.close();
|
|
96
|
+
client.close();
|
|
97
|
+
}, 'multiple close calls should not throw');
|
|
98
|
+
|
|
99
|
+
test.ok(client.isClosed, 'client should be marked as closed');
|
|
100
|
+
|
|
101
|
+
test.done();
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
'should properly set state on close'(test) {
|
|
105
|
+
const client = new ImapFlow({
|
|
106
|
+
host: '127.0.0.1',
|
|
107
|
+
port: 1,
|
|
108
|
+
secure: false,
|
|
109
|
+
logger: false
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test.equal(client.state, client.states.NOT_AUTHENTICATED, 'initial state should be NOT_AUTHENTICATED');
|
|
113
|
+
test.equal(client.usable, false, 'usable should be false initially');
|
|
114
|
+
test.equal(client.isClosed, false, 'isClosed should be false initially');
|
|
115
|
+
|
|
116
|
+
client.close();
|
|
117
|
+
|
|
118
|
+
test.equal(client.state, client.states.LOGOUT, 'state should be LOGOUT after close');
|
|
119
|
+
test.equal(client.usable, false, 'usable should be false after close');
|
|
120
|
+
test.equal(client.isClosed, true, 'isClosed should be true after close');
|
|
121
|
+
|
|
122
|
+
test.done();
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
'should emit close event'(test) {
|
|
126
|
+
const client = new ImapFlow({
|
|
127
|
+
host: '127.0.0.1',
|
|
128
|
+
port: 1,
|
|
129
|
+
secure: false,
|
|
130
|
+
logger: false
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
let closeEmitted = false;
|
|
134
|
+
client.on('close', () => {
|
|
135
|
+
closeEmitted = true;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
client.close();
|
|
139
|
+
|
|
140
|
+
test.ok(closeEmitted, 'close event should be emitted');
|
|
141
|
+
|
|
142
|
+
test.done();
|
|
143
|
+
}
|
|
144
|
+
};
|