imapflow 1.2.3 → 1.2.5

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.
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ const { JPDecoder } = require('../lib/jp-decoder');
4
+ const { PassThrough, Transform } = require('stream');
5
+
6
+ // Helper to collect stream output
7
+ const collectStream = stream =>
8
+ new Promise((resolve, reject) => {
9
+ const chunks = [];
10
+ stream.on('data', chunk => chunks.push(chunk));
11
+ stream.on('end', () => resolve(Buffer.concat(chunks)));
12
+ stream.on('error', reject);
13
+ });
14
+
15
+ // ============================================
16
+ // Constructor tests
17
+ // ============================================
18
+
19
+ module.exports['JPDecoder: constructor sets charset'] = test => {
20
+ let decoder = new JPDecoder('iso-2022-jp');
21
+ test.equal(decoder.charset, 'iso-2022-jp');
22
+ test.deepEqual(decoder.chunks, []);
23
+ test.equal(decoder.chunklen, 0);
24
+ test.done();
25
+ };
26
+
27
+ module.exports['JPDecoder: is a Transform stream'] = test => {
28
+ let decoder = new JPDecoder('iso-2022-jp');
29
+ test.ok(decoder instanceof Transform);
30
+ test.ok(typeof decoder.pipe === 'function');
31
+ test.ok(typeof decoder.write === 'function');
32
+ test.done();
33
+ };
34
+
35
+ // ============================================
36
+ // _transform tests
37
+ // ============================================
38
+
39
+ module.exports['JPDecoder: _transform accumulates buffer chunks'] = test => {
40
+ let decoder = new JPDecoder('iso-2022-jp');
41
+ let chunk1 = Buffer.from('hello');
42
+ let chunk2 = Buffer.from('world');
43
+
44
+ decoder._transform(chunk1, 'buffer', () => {
45
+ test.equal(decoder.chunks.length, 1);
46
+ test.equal(decoder.chunklen, 5);
47
+
48
+ decoder._transform(chunk2, 'buffer', () => {
49
+ test.equal(decoder.chunks.length, 2);
50
+ test.equal(decoder.chunklen, 10);
51
+ test.done();
52
+ });
53
+ });
54
+ };
55
+
56
+ module.exports['JPDecoder: _transform converts string to buffer'] = test => {
57
+ let decoder = new JPDecoder('iso-2022-jp');
58
+ let stringChunk = 'hello';
59
+
60
+ decoder._transform(stringChunk, 'utf8', () => {
61
+ test.equal(decoder.chunks.length, 1);
62
+ test.ok(Buffer.isBuffer(decoder.chunks[0]));
63
+ test.equal(decoder.chunks[0].toString(), 'hello');
64
+ test.done();
65
+ });
66
+ };
67
+
68
+ // ============================================
69
+ // _flush tests
70
+ // ============================================
71
+
72
+ module.exports['JPDecoder: _flush outputs accumulated data'] = async test => {
73
+ let decoder = new JPDecoder('utf-8');
74
+ let output = collectStream(decoder);
75
+
76
+ decoder.write(Buffer.from('hello '));
77
+ decoder.write(Buffer.from('world'));
78
+ decoder.end();
79
+
80
+ let result = await output;
81
+ test.equal(result.toString(), 'hello world');
82
+ test.done();
83
+ };
84
+
85
+ module.exports['JPDecoder: _flush converts ISO-2022-JP to Unicode'] = async test => {
86
+ let decoder = new JPDecoder('iso-2022-jp');
87
+ let output = collectStream(decoder);
88
+
89
+ // ISO-2022-JP encoded Japanese text for "nihongo" (Japanese)
90
+ // ESC $ B sequence switches to JIS X 0208, ESC ( B switches back to ASCII
91
+ let iso2022jp = Buffer.from([
92
+ 0x1b,
93
+ 0x24,
94
+ 0x42, // ESC $ B - switch to JIS X 0208
95
+ 0x46,
96
+ 0x7c, // ni
97
+ 0x4b,
98
+ 0x5c, // hon
99
+ 0x38,
100
+ 0x6c, // go
101
+ 0x1b,
102
+ 0x28,
103
+ 0x42 // ESC ( B - switch back to ASCII
104
+ ]);
105
+
106
+ decoder.write(iso2022jp);
107
+ decoder.end();
108
+
109
+ let result = await output;
110
+ test.ok(result.length > 0);
111
+ test.done();
112
+ };
113
+
114
+ module.exports['JPDecoder: _flush handles conversion errors gracefully'] = async test => {
115
+ // Use an invalid/unknown charset to trigger error path
116
+ let decoder = new JPDecoder('invalid-charset-xyz');
117
+ let output = collectStream(decoder);
118
+
119
+ let input = Buffer.from('test data');
120
+ decoder.write(input);
121
+ decoder.end();
122
+
123
+ let result = await output;
124
+ // On error, should return original input
125
+ test.equal(result.toString(), 'test data');
126
+ test.done();
127
+ };
128
+
129
+ module.exports['JPDecoder: _flush handles empty input'] = async test => {
130
+ let decoder = new JPDecoder('iso-2022-jp');
131
+ let output = collectStream(decoder);
132
+
133
+ decoder.end();
134
+
135
+ let result = await output;
136
+ test.equal(result.length, 0);
137
+ test.done();
138
+ };
139
+
140
+ // ============================================
141
+ // Integration tests
142
+ // ============================================
143
+
144
+ module.exports['JPDecoder: works with pipe'] = async test => {
145
+ let source = new PassThrough();
146
+ let decoder = new JPDecoder('utf-8');
147
+ let output = collectStream(source.pipe(decoder));
148
+
149
+ source.write('hello ');
150
+ source.write('world');
151
+ source.end();
152
+
153
+ let result = await output;
154
+ test.equal(result.toString(), 'hello world');
155
+ test.done();
156
+ };
157
+
158
+ module.exports['JPDecoder: handles multiple small chunks'] = async test => {
159
+ let decoder = new JPDecoder('utf-8');
160
+ let output = collectStream(decoder);
161
+
162
+ // Write character by character
163
+ 'hello'.split('').forEach(char => decoder.write(char));
164
+ decoder.end();
165
+
166
+ let result = await output;
167
+ test.equal(result.toString(), 'hello');
168
+ test.done();
169
+ };
170
+
171
+ module.exports['JPDecoder: handles Shift_JIS charset'] = async test => {
172
+ let decoder = new JPDecoder('shift_jis');
173
+ let output = collectStream(decoder);
174
+
175
+ // Shift_JIS encoded "test" in katakana (tesuto)
176
+ let shiftJis = Buffer.from([0x83, 0x65, 0x83, 0x58, 0x83, 0x67]);
177
+
178
+ decoder.write(shiftJis);
179
+ decoder.end();
180
+
181
+ let result = await output;
182
+ test.ok(result.length > 0);
183
+ test.done();
184
+ };
185
+
186
+ module.exports['JPDecoder: handles EUC-JP charset'] = async test => {
187
+ let decoder = new JPDecoder('euc-jp');
188
+ let output = collectStream(decoder);
189
+
190
+ // EUC-JP encoded Japanese character
191
+ let eucJp = Buffer.from([0xc6, 0xfc, 0xcb, 0xdc]); // nihon
192
+
193
+ decoder.write(eucJp);
194
+ decoder.end();
195
+
196
+ let result = await output;
197
+ test.ok(result.length > 0);
198
+ test.done();
199
+ };
@@ -0,0 +1,258 @@
1
+ 'use strict';
2
+
3
+ const { LimitedPassthrough } = require('../lib/limited-passthrough');
4
+ const { PassThrough, Transform } = require('stream');
5
+
6
+ // Helper to collect stream output
7
+ const collectStream = stream =>
8
+ new Promise((resolve, reject) => {
9
+ const chunks = [];
10
+ stream.on('data', chunk => chunks.push(chunk));
11
+ stream.on('end', () => resolve(Buffer.concat(chunks)));
12
+ stream.on('error', reject);
13
+ });
14
+
15
+ // ============================================
16
+ // Constructor tests
17
+ // ============================================
18
+
19
+ module.exports['LimitedPassthrough: constructor with options'] = test => {
20
+ let stream = new LimitedPassthrough({ maxBytes: 100 });
21
+ test.equal(stream.maxBytes, 100);
22
+ test.equal(stream.processed, 0);
23
+ test.equal(stream.limited, false);
24
+ test.done();
25
+ };
26
+
27
+ module.exports['LimitedPassthrough: constructor with no options'] = test => {
28
+ let stream = new LimitedPassthrough();
29
+ test.equal(stream.maxBytes, Infinity);
30
+ test.equal(stream.processed, 0);
31
+ test.equal(stream.limited, false);
32
+ test.done();
33
+ };
34
+
35
+ module.exports['LimitedPassthrough: constructor with null options'] = test => {
36
+ let stream = new LimitedPassthrough(null);
37
+ test.equal(stream.maxBytes, Infinity);
38
+ test.done();
39
+ };
40
+
41
+ module.exports['LimitedPassthrough: is a Transform stream'] = test => {
42
+ let stream = new LimitedPassthrough();
43
+ test.ok(stream instanceof Transform);
44
+ test.ok(typeof stream.pipe === 'function');
45
+ test.ok(typeof stream.write === 'function');
46
+ test.done();
47
+ };
48
+
49
+ // ============================================
50
+ // _transform tests - no limit
51
+ // ============================================
52
+
53
+ module.exports['LimitedPassthrough: passes all data when no limit'] = async test => {
54
+ let stream = new LimitedPassthrough();
55
+ let output = collectStream(stream);
56
+
57
+ stream.write(Buffer.from('hello '));
58
+ stream.write(Buffer.from('world'));
59
+ stream.end();
60
+
61
+ let result = await output;
62
+ test.equal(result.toString(), 'hello world');
63
+ test.equal(stream.processed, 11);
64
+ test.equal(stream.limited, false);
65
+ test.done();
66
+ };
67
+
68
+ // ============================================
69
+ // _transform tests - with limit
70
+ // ============================================
71
+
72
+ module.exports['LimitedPassthrough: limits output to maxBytes'] = async test => {
73
+ let stream = new LimitedPassthrough({ maxBytes: 5 });
74
+ let output = collectStream(stream);
75
+
76
+ stream.write(Buffer.from('hello world'));
77
+ stream.end();
78
+
79
+ let result = await output;
80
+ test.equal(result.toString(), 'hello');
81
+ test.equal(stream.processed, 5);
82
+ test.equal(stream.limited, true);
83
+ test.done();
84
+ };
85
+
86
+ module.exports['LimitedPassthrough: limits across multiple chunks'] = async test => {
87
+ let stream = new LimitedPassthrough({ maxBytes: 8 });
88
+ let output = collectStream(stream);
89
+
90
+ stream.write(Buffer.from('hello ')); // 6 bytes
91
+ stream.write(Buffer.from('world')); // 5 bytes, only 2 should pass
92
+ stream.end();
93
+
94
+ let result = await output;
95
+ test.equal(result.toString(), 'hello wo');
96
+ test.equal(stream.processed, 8);
97
+ test.equal(stream.limited, true);
98
+ test.done();
99
+ };
100
+
101
+ module.exports['LimitedPassthrough: drops data after limit reached'] = async test => {
102
+ let stream = new LimitedPassthrough({ maxBytes: 5 });
103
+ let output = collectStream(stream);
104
+
105
+ stream.write(Buffer.from('hello')); // exactly 5 bytes
106
+ stream.write(Buffer.from(' world')); // should be dropped
107
+ stream.write(Buffer.from('!')); // should be dropped
108
+ stream.end();
109
+
110
+ let result = await output;
111
+ test.equal(result.toString(), 'hello');
112
+ test.equal(stream.limited, true);
113
+ test.done();
114
+ };
115
+
116
+ module.exports['LimitedPassthrough: handles exact boundary'] = async test => {
117
+ let stream = new LimitedPassthrough({ maxBytes: 5 });
118
+ let output = collectStream(stream);
119
+
120
+ stream.write(Buffer.from('hello')); // exactly 5 bytes
121
+ stream.end();
122
+
123
+ let result = await output;
124
+ test.equal(result.toString(), 'hello');
125
+ test.equal(stream.processed, 5);
126
+ test.equal(stream.limited, true);
127
+ test.done();
128
+ };
129
+
130
+ module.exports['LimitedPassthrough: zero maxBytes treated as Infinity'] = async test => {
131
+ // Note: maxBytes: 0 is falsy, so constructor uses Infinity instead
132
+ let stream = new LimitedPassthrough({ maxBytes: 0 });
133
+ test.equal(stream.maxBytes, Infinity);
134
+
135
+ let output = collectStream(stream);
136
+ stream.write(Buffer.from('hello'));
137
+ stream.end();
138
+
139
+ let result = await output;
140
+ // All data passes through since 0 is treated as Infinity
141
+ test.equal(result.toString(), 'hello');
142
+ test.done();
143
+ };
144
+
145
+ module.exports['LimitedPassthrough: handles limit of 1 byte'] = async test => {
146
+ let stream = new LimitedPassthrough({ maxBytes: 1 });
147
+ let output = collectStream(stream);
148
+
149
+ stream.write(Buffer.from('hello'));
150
+ stream.end();
151
+
152
+ let result = await output;
153
+ test.equal(result.toString(), 'h');
154
+ test.equal(stream.processed, 1);
155
+ test.equal(stream.limited, true);
156
+ test.done();
157
+ };
158
+
159
+ // ============================================
160
+ // Edge cases
161
+ // ============================================
162
+
163
+ module.exports['LimitedPassthrough: handles empty writes'] = async test => {
164
+ let stream = new LimitedPassthrough({ maxBytes: 10 });
165
+ let output = collectStream(stream);
166
+
167
+ stream.write(Buffer.from(''));
168
+ stream.write(Buffer.from('hello'));
169
+ stream.write(Buffer.from(''));
170
+ stream.end();
171
+
172
+ let result = await output;
173
+ test.equal(result.toString(), 'hello');
174
+ test.equal(stream.processed, 5);
175
+ test.done();
176
+ };
177
+
178
+ module.exports['LimitedPassthrough: handles no writes'] = async test => {
179
+ let stream = new LimitedPassthrough({ maxBytes: 10 });
180
+ let output = collectStream(stream);
181
+
182
+ stream.end();
183
+
184
+ let result = await output;
185
+ test.equal(result.length, 0);
186
+ test.equal(stream.processed, 0);
187
+ test.equal(stream.limited, false);
188
+ test.done();
189
+ };
190
+
191
+ module.exports['LimitedPassthrough: tracks processed bytes correctly'] = async test => {
192
+ let stream = new LimitedPassthrough({ maxBytes: 100 });
193
+ let output = collectStream(stream);
194
+
195
+ stream.write(Buffer.from('12345')); // 5 bytes
196
+ test.equal(stream.processed, 5);
197
+
198
+ stream.write(Buffer.from('67890')); // 5 more bytes
199
+ test.equal(stream.processed, 10);
200
+
201
+ stream.end();
202
+
203
+ await output;
204
+ test.equal(stream.processed, 10);
205
+ test.done();
206
+ };
207
+
208
+ // ============================================
209
+ // Integration tests
210
+ // ============================================
211
+
212
+ module.exports['LimitedPassthrough: works with pipe'] = async test => {
213
+ let source = new PassThrough();
214
+ let limiter = new LimitedPassthrough({ maxBytes: 10 });
215
+ let output = collectStream(source.pipe(limiter));
216
+
217
+ source.write('hello ');
218
+ source.write('wonderful ');
219
+ source.write('world');
220
+ source.end();
221
+
222
+ let result = await output;
223
+ test.equal(result.toString(), 'hello wond');
224
+ test.equal(limiter.limited, true);
225
+ test.done();
226
+ };
227
+
228
+ module.exports['LimitedPassthrough: handles large data'] = async test => {
229
+ let stream = new LimitedPassthrough({ maxBytes: 1000 });
230
+ let output = collectStream(stream);
231
+
232
+ // Write 100 bytes at a time
233
+ for (let i = 0; i < 20; i++) {
234
+ stream.write(Buffer.alloc(100, 'x'));
235
+ }
236
+ stream.end();
237
+
238
+ let result = await output;
239
+ test.equal(result.length, 1000);
240
+ test.equal(stream.limited, true);
241
+ test.done();
242
+ };
243
+
244
+ module.exports['LimitedPassthrough: single byte writes'] = async test => {
245
+ let stream = new LimitedPassthrough({ maxBytes: 3 });
246
+ let output = collectStream(stream);
247
+
248
+ stream.write(Buffer.from('a'));
249
+ stream.write(Buffer.from('b'));
250
+ stream.write(Buffer.from('c'));
251
+ stream.write(Buffer.from('d')); // should be dropped
252
+ stream.write(Buffer.from('e')); // should be dropped
253
+ stream.end();
254
+
255
+ let result = await output;
256
+ test.equal(result.toString(), 'abc');
257
+ test.done();
258
+ };
@@ -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
+ };