imapflow 1.2.13 → 1.2.14

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,275 @@
1
+ /* eslint new-cap: 0 */
2
+ 'use strict';
3
+
4
+ const imapFormalSyntax = require('../lib/handler/imap-formal-syntax');
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // CHAR
8
+ // ---------------------------------------------------------------------------
9
+
10
+ module.exports['Formal Syntax: CHAR() has length 127'] = test => {
11
+ test.equal(imapFormalSyntax.CHAR().length, 127);
12
+ test.done();
13
+ };
14
+
15
+ module.exports['Formal Syntax: CHAR() includes A'] = test => {
16
+ test.ok(imapFormalSyntax.CHAR().includes('A'));
17
+ test.done();
18
+ };
19
+
20
+ module.exports['Formal Syntax: CHAR() excludes NUL (0x00)'] = test => {
21
+ test.ok(!imapFormalSyntax.CHAR().includes('\x00'));
22
+ test.done();
23
+ };
24
+
25
+ module.exports['Formal Syntax: CHAR() excludes 0x80'] = test => {
26
+ test.ok(!imapFormalSyntax.CHAR().includes('\x80'));
27
+ test.done();
28
+ };
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // CHAR8
32
+ // ---------------------------------------------------------------------------
33
+
34
+ module.exports['Formal Syntax: CHAR8() has length 255'] = test => {
35
+ test.equal(imapFormalSyntax.CHAR8().length, 255);
36
+ test.done();
37
+ };
38
+
39
+ module.exports['Formal Syntax: CHAR8() includes 0xFF'] = test => {
40
+ test.ok(imapFormalSyntax.CHAR8().includes('\xFF'));
41
+ test.done();
42
+ };
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // SP
46
+ // ---------------------------------------------------------------------------
47
+
48
+ module.exports['Formal Syntax: SP() equals a single space'] = test => {
49
+ test.equal(imapFormalSyntax.SP(), ' ');
50
+ test.done();
51
+ };
52
+
53
+ module.exports['Formal Syntax: SP() has length 1'] = test => {
54
+ test.equal(imapFormalSyntax.SP().length, 1);
55
+ test.done();
56
+ };
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // CTL
60
+ // ---------------------------------------------------------------------------
61
+
62
+ module.exports['Formal Syntax: CTL() includes NUL (0x00)'] = test => {
63
+ test.ok(imapFormalSyntax.CTL().includes('\x00'));
64
+ test.done();
65
+ };
66
+
67
+ module.exports['Formal Syntax: CTL() includes 0x1F'] = test => {
68
+ test.ok(imapFormalSyntax.CTL().includes('\x1F'));
69
+ test.done();
70
+ };
71
+
72
+ module.exports['Formal Syntax: CTL() includes DEL (0x7F)'] = test => {
73
+ test.ok(imapFormalSyntax.CTL().includes('\x7F'));
74
+ test.done();
75
+ };
76
+
77
+ module.exports['Formal Syntax: CTL() excludes space (0x20)'] = test => {
78
+ test.ok(!imapFormalSyntax.CTL().includes(' '));
79
+ test.done();
80
+ };
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // ALPHA
84
+ // ---------------------------------------------------------------------------
85
+
86
+ module.exports['Formal Syntax: ALPHA() has length 52'] = test => {
87
+ test.equal(imapFormalSyntax.ALPHA().length, 52);
88
+ test.done();
89
+ };
90
+
91
+ module.exports['Formal Syntax: ALPHA() includes A'] = test => {
92
+ test.ok(imapFormalSyntax.ALPHA().includes('A'));
93
+ test.done();
94
+ };
95
+
96
+ module.exports['Formal Syntax: ALPHA() includes z'] = test => {
97
+ test.ok(imapFormalSyntax.ALPHA().includes('z'));
98
+ test.done();
99
+ };
100
+
101
+ module.exports['Formal Syntax: ALPHA() excludes 0'] = test => {
102
+ test.ok(!imapFormalSyntax.ALPHA().includes('0'));
103
+ test.done();
104
+ };
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // DIGIT
108
+ // ---------------------------------------------------------------------------
109
+
110
+ module.exports['Formal Syntax: DIGIT() has length 10'] = test => {
111
+ test.equal(imapFormalSyntax.DIGIT().length, 10);
112
+ test.done();
113
+ };
114
+
115
+ module.exports['Formal Syntax: DIGIT() includes 0'] = test => {
116
+ test.ok(imapFormalSyntax.DIGIT().includes('0'));
117
+ test.done();
118
+ };
119
+
120
+ module.exports['Formal Syntax: DIGIT() includes 9'] = test => {
121
+ test.ok(imapFormalSyntax.DIGIT().includes('9'));
122
+ test.done();
123
+ };
124
+
125
+ module.exports['Formal Syntax: DIGIT() excludes a'] = test => {
126
+ test.ok(!imapFormalSyntax.DIGIT().includes('a'));
127
+ test.done();
128
+ };
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // ATOM-CHAR
132
+ // ---------------------------------------------------------------------------
133
+
134
+ module.exports['Formal Syntax: ATOM-CHAR() includes A'] = test => {
135
+ test.ok(imapFormalSyntax['ATOM-CHAR']().includes('A'));
136
+ test.done();
137
+ };
138
+
139
+ module.exports['Formal Syntax: ATOM-CHAR() excludes open paren'] = test => {
140
+ test.ok(!imapFormalSyntax['ATOM-CHAR']().includes('('));
141
+ test.done();
142
+ };
143
+
144
+ module.exports['Formal Syntax: ATOM-CHAR() excludes close paren'] = test => {
145
+ test.ok(!imapFormalSyntax['ATOM-CHAR']().includes(')'));
146
+ test.done();
147
+ };
148
+
149
+ module.exports['Formal Syntax: ATOM-CHAR() excludes open brace'] = test => {
150
+ test.ok(!imapFormalSyntax['ATOM-CHAR']().includes('{'));
151
+ test.done();
152
+ };
153
+
154
+ module.exports['Formal Syntax: ATOM-CHAR() excludes space'] = test => {
155
+ test.ok(!imapFormalSyntax['ATOM-CHAR']().includes(' '));
156
+ test.done();
157
+ };
158
+
159
+ module.exports['Formal Syntax: ATOM-CHAR() excludes double quote'] = test => {
160
+ test.ok(!imapFormalSyntax['ATOM-CHAR']().includes('"'));
161
+ test.done();
162
+ };
163
+
164
+ module.exports['Formal Syntax: ATOM-CHAR() excludes backslash'] = test => {
165
+ test.ok(!imapFormalSyntax['ATOM-CHAR']().includes('\\'));
166
+ test.done();
167
+ };
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // ASTRING-CHAR
171
+ // ---------------------------------------------------------------------------
172
+
173
+ module.exports['Formal Syntax: ASTRING-CHAR() includes close bracket (resp-special)'] = test => {
174
+ test.ok(imapFormalSyntax['ASTRING-CHAR']().includes(']'));
175
+ test.done();
176
+ };
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // TEXT-CHAR
180
+ // ---------------------------------------------------------------------------
181
+
182
+ module.exports['Formal Syntax: TEXT-CHAR() includes A'] = test => {
183
+ test.ok(imapFormalSyntax['TEXT-CHAR']().includes('A'));
184
+ test.done();
185
+ };
186
+
187
+ module.exports['Formal Syntax: TEXT-CHAR() includes space'] = test => {
188
+ test.ok(imapFormalSyntax['TEXT-CHAR']().includes(' '));
189
+ test.done();
190
+ };
191
+
192
+ module.exports['Formal Syntax: TEXT-CHAR() excludes CR'] = test => {
193
+ test.ok(!imapFormalSyntax['TEXT-CHAR']().includes('\r'));
194
+ test.done();
195
+ };
196
+
197
+ module.exports['Formal Syntax: TEXT-CHAR() excludes LF'] = test => {
198
+ test.ok(!imapFormalSyntax['TEXT-CHAR']().includes('\n'));
199
+ test.done();
200
+ };
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // tag
204
+ // ---------------------------------------------------------------------------
205
+
206
+ module.exports['Formal Syntax: tag() excludes plus sign'] = test => {
207
+ test.ok(!imapFormalSyntax.tag().includes('+'));
208
+ test.done();
209
+ };
210
+
211
+ module.exports['Formal Syntax: tag() includes A'] = test => {
212
+ test.ok(imapFormalSyntax.tag().includes('A'));
213
+ test.done();
214
+ };
215
+
216
+ module.exports['Formal Syntax: tag() includes close bracket'] = test => {
217
+ test.ok(imapFormalSyntax.tag().includes(']'));
218
+ test.done();
219
+ };
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // command
223
+ // ---------------------------------------------------------------------------
224
+
225
+ module.exports['Formal Syntax: command() includes A'] = test => {
226
+ test.ok(imapFormalSyntax.command().includes('A'));
227
+ test.done();
228
+ };
229
+
230
+ module.exports['Formal Syntax: command() includes 0'] = test => {
231
+ test.ok(imapFormalSyntax.command().includes('0'));
232
+ test.done();
233
+ };
234
+
235
+ module.exports['Formal Syntax: command() includes hyphen'] = test => {
236
+ test.ok(imapFormalSyntax.command().includes('-'));
237
+ test.done();
238
+ };
239
+
240
+ module.exports['Formal Syntax: command() excludes asterisk'] = test => {
241
+ test.ok(!imapFormalSyntax.command().includes('*'));
242
+ test.done();
243
+ };
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // verify()
247
+ // ---------------------------------------------------------------------------
248
+
249
+ module.exports['Formal Syntax: verify() returns -1 when all characters are valid'] = test => {
250
+ test.equal(imapFormalSyntax.verify('ABC', 'ABCDEF'), -1);
251
+ test.done();
252
+ };
253
+
254
+ module.exports['Formal Syntax: verify() returns index of first invalid character'] = test => {
255
+ test.equal(imapFormalSyntax.verify('ABXC', 'ABC'), 2);
256
+ test.done();
257
+ };
258
+
259
+ module.exports['Formal Syntax: verify() returns 0 when first character is invalid'] = test => {
260
+ test.equal(imapFormalSyntax.verify('!A', 'ABC'), 0);
261
+ test.done();
262
+ };
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Memoization
266
+ // ---------------------------------------------------------------------------
267
+
268
+ module.exports['Formal Syntax: CHAR() returns same string reference on repeated calls (memoized)'] = test => {
269
+ // After the first call, the method is replaced with a closure that returns
270
+ // the cached value, so subsequent calls must return the exact same string object.
271
+ const first = imapFormalSyntax.CHAR();
272
+ const second = imapFormalSyntax.CHAR();
273
+ test.strictEqual(first, second);
274
+ test.done();
275
+ };
@@ -0,0 +1,268 @@
1
+ /*eslint no-unused-expressions: 0, prefer-arrow-callback: 0 */
2
+
3
+ 'use strict';
4
+
5
+ const { ImapStream } = require('../lib/handler/imap-stream');
6
+
7
+ /**
8
+ * Helper that wires up the standard readable/next consumer pattern used across tests.
9
+ *
10
+ * The reader calls onCommand synchronously for each command object, then calls cmd.next()
11
+ * to allow the stream to continue. test.done() is called on the 'end' event.
12
+ *
13
+ * The readable handler uses a pendingRead flag so that if a 'readable' event fires
14
+ * while the reader is active, a follow-up read is triggered after the reader finishes.
15
+ * This prevents the 'end' event from being missed when the stream ends between reader
16
+ * iterations.
17
+ *
18
+ * @param {Object} test - nodeunit test object
19
+ * @param {Function} onCommand - synchronous function(cmd) called for each parsed command
20
+ * @param {Function} writer - async function(stream) that writes data to the stream
21
+ * @param {number} [expectedCount] - if set, assert that exactly this many commands were emitted
22
+ */
23
+ function runStreamTest(test, onCommand, writer, expectedCount) {
24
+ const stream = new ImapStream({ cid: 'test' });
25
+ let commandCount = 0;
26
+
27
+ let reading = false;
28
+ let pendingRead = false;
29
+
30
+ const reader = async () => {
31
+ let cmd;
32
+ while ((cmd = stream.read()) !== null) {
33
+ commandCount++;
34
+ onCommand(cmd);
35
+ cmd.next();
36
+ }
37
+ };
38
+
39
+ const drainStream = () => {
40
+ if (reading) {
41
+ pendingRead = true;
42
+ return;
43
+ }
44
+ reading = true;
45
+ reader()
46
+ .catch(err => test.ifError(err))
47
+ .finally(() => {
48
+ reading = false;
49
+ if (pendingRead) {
50
+ pendingRead = false;
51
+ drainStream();
52
+ }
53
+ });
54
+ };
55
+
56
+ stream.on('readable', drainStream);
57
+
58
+ stream.on('error', err => {
59
+ test.ifError(err);
60
+ });
61
+
62
+ stream.on('end', () => {
63
+ if (expectedCount !== undefined) {
64
+ test.equal(commandCount, expectedCount, `expected ${expectedCount} command(s), got ${commandCount}`);
65
+ }
66
+ test.done();
67
+ });
68
+
69
+ writer(stream).catch(err => test.ifError(err));
70
+ }
71
+
72
+ module.exports['Literal split across chunks'] = test => {
73
+ runStreamTest(
74
+ test,
75
+ cmd => {
76
+ test.equal(cmd.payload.toString(), 'A APPEND {5}\r\n', 'payload should include literal marker line');
77
+ test.equal(cmd.literals.length, 1, 'should have one literal');
78
+ test.ok(Buffer.isBuffer(cmd.literals[0]), 'literal should be a Buffer');
79
+ test.equal(cmd.literals[0].toString(), '12345', 'literal content should be 12345');
80
+ },
81
+ async stream => {
82
+ stream.write(Buffer.from('A APPEND {5}\r\n'));
83
+ stream.end(Buffer.from('12345\r\n'));
84
+ },
85
+ 1
86
+ );
87
+ };
88
+
89
+ module.exports['Literal with zero size'] = test => {
90
+ runStreamTest(
91
+ test,
92
+ cmd => {
93
+ test.equal(cmd.payload.toString(), 'A APPEND {0}\r\n', 'payload should include zero-size literal marker');
94
+ test.equal(cmd.literals.length, 1, 'should have one literal');
95
+ test.ok(Buffer.isBuffer(cmd.literals[0]), 'literal should be a Buffer');
96
+ test.equal(cmd.literals[0].length, 0, 'literal should be empty (length 0)');
97
+ },
98
+ async stream => {
99
+ stream.end(Buffer.from('A APPEND {0}\r\n\r\n'));
100
+ },
101
+ 1
102
+ );
103
+ };
104
+
105
+ module.exports['Multiple commands in single chunk'] = test => {
106
+ const expected = ['A CMD1', 'B CMD2'];
107
+ let index = 0;
108
+
109
+ runStreamTest(
110
+ test,
111
+ cmd => {
112
+ test.equal(cmd.payload.toString(), expected[index], `command ${index} payload`);
113
+ index++;
114
+ },
115
+ async stream => {
116
+ stream.end(Buffer.from('A CMD1\r\nB CMD2\r\n'));
117
+ },
118
+ 2
119
+ );
120
+ };
121
+
122
+ module.exports['LiteralTooLarge error'] = test => {
123
+ const stream = new ImapStream({ cid: 'test' });
124
+
125
+ stream.on('error', err => {
126
+ test.equal(err.code, 'LiteralTooLarge', 'error code should be LiteralTooLarge');
127
+ stream.destroy();
128
+ test.done();
129
+ });
130
+
131
+ stream.write(Buffer.from('A APPEND {1073741825}\r\n'));
132
+ };
133
+
134
+ module.exports['Incomplete line continued in next chunk'] = test => {
135
+ runStreamTest(
136
+ test,
137
+ cmd => {
138
+ test.equal(cmd.payload.toString(), 'A CAPABILITY', 'payload should be A CAPABILITY');
139
+ test.equal(cmd.literals.length, 0, 'should have no literals');
140
+ },
141
+ async stream => {
142
+ stream.write(Buffer.from('A CA'));
143
+ stream.end(Buffer.from('PABILITY\r\n'));
144
+ },
145
+ 1
146
+ );
147
+ };
148
+
149
+ module.exports['Empty chunk then valid command'] = test => {
150
+ runStreamTest(
151
+ test,
152
+ cmd => {
153
+ test.equal(cmd.payload.toString(), 'A CMD', 'payload should be A CMD');
154
+ test.equal(cmd.literals.length, 0, 'should have no literals');
155
+ },
156
+ async stream => {
157
+ stream.write(Buffer.alloc(0));
158
+ stream.end(Buffer.from('A CMD\r\n'));
159
+ },
160
+ 1
161
+ );
162
+ };
163
+
164
+ module.exports['String input converted to Buffer'] = test => {
165
+ runStreamTest(
166
+ test,
167
+ cmd => {
168
+ test.equal(cmd.payload.toString(), 'A CMD', 'payload should be A CMD');
169
+ test.ok(Buffer.isBuffer(cmd.payload), 'payload should be a Buffer');
170
+ test.equal(cmd.literals.length, 0, 'should have no literals');
171
+ },
172
+ async stream => {
173
+ stream.end('A CMD\r\n');
174
+ },
175
+ 1
176
+ );
177
+ };
178
+
179
+ module.exports['LF-only line terminator'] = test => {
180
+ runStreamTest(
181
+ test,
182
+ cmd => {
183
+ test.equal(cmd.payload.toString(), 'A CMD', 'payload should be A CMD without CR or LF');
184
+ test.equal(cmd.literals.length, 0, 'should have no literals');
185
+ },
186
+ async stream => {
187
+ stream.end(Buffer.from('A CMD\n'));
188
+ },
189
+ 1
190
+ );
191
+ };
192
+
193
+ module.exports['Many chunks trigger event loop yield'] = test => {
194
+ runStreamTest(
195
+ test,
196
+ cmd => {
197
+ // Just verify each command is valid
198
+ test.ok(cmd.payload.toString().startsWith('A'), 'command should start with tag');
199
+ },
200
+ async stream => {
201
+ for (let i = 0; i < 15; i++) {
202
+ stream.write(Buffer.from(`A CMD${i}\r\n`));
203
+ }
204
+ stream.end();
205
+ },
206
+ 15
207
+ );
208
+ };
209
+
210
+ module.exports['Destroy with queued items does not hang'] = test => {
211
+ const stream = new ImapStream({ cid: 'test' });
212
+ let errorEmitted = false;
213
+
214
+ stream.on('error', () => {
215
+ errorEmitted = true;
216
+ });
217
+
218
+ // Write multiple chunks rapidly then destroy
219
+ stream.write(Buffer.from('A CMD1\r\n'));
220
+ stream.write(Buffer.from('B CMD2\r\n'));
221
+ stream.destroy();
222
+
223
+ // Errors from destroy are emitted synchronously or on next tick
224
+ setImmediate(() => {
225
+ test.ok(!errorEmitted, 'should not emit error on destroy');
226
+ test.done();
227
+ });
228
+ };
229
+
230
+ module.exports['logRaw option triggers trace logging'] = test => {
231
+ let traceCalled = false;
232
+ let traceData = null;
233
+
234
+ const stream = new ImapStream({
235
+ cid: 'test',
236
+ logRaw: true
237
+ });
238
+
239
+ // Override the log object to capture trace calls
240
+ stream.log = {
241
+ trace: data => {
242
+ traceCalled = true;
243
+ traceData = data;
244
+ },
245
+ debug: () => {},
246
+ info: () => {},
247
+ warn: () => {},
248
+ error: () => {}
249
+ };
250
+
251
+ // Also need to handle readable events
252
+ stream.on('readable', () => {
253
+ let cmd;
254
+ while ((cmd = stream.read()) !== null) {
255
+ cmd.next();
256
+ }
257
+ });
258
+
259
+ stream.on('end', () => {
260
+ test.ok(traceCalled, 'trace should have been called');
261
+ test.ok(traceData, 'trace data should exist');
262
+ test.equal(traceData.src, 's', 'source should be s');
263
+ test.ok(traceData.data, 'should have base64 data');
264
+ test.done();
265
+ });
266
+
267
+ stream.end(Buffer.from('A CMD\r\n'));
268
+ };