imapflow 1.7.7 → 2.0.0

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.
Files changed (296) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +8 -2
  3. package/dist/cjs/charsets.d.ts +1 -0
  4. package/dist/cjs/charsets.js +294 -0
  5. package/dist/cjs/commands/append.d.ts +22 -0
  6. package/dist/cjs/commands/append.js +151 -0
  7. package/dist/cjs/commands/authenticate.d.ts +24 -0
  8. package/dist/cjs/commands/authenticate.js +223 -0
  9. package/dist/cjs/commands/capability.d.ts +8 -0
  10. package/dist/cjs/commands/capability.js +32 -0
  11. package/dist/cjs/commands/close.d.ts +8 -0
  12. package/dist/cjs/commands/close.js +39 -0
  13. package/dist/cjs/commands/compress.d.ts +8 -0
  14. package/dist/cjs/commands/compress.js +56 -0
  15. package/dist/cjs/commands/copy.d.ts +13 -0
  16. package/dist/cjs/commands/copy.js +44 -0
  17. package/dist/cjs/commands/copyuid-parser.d.ts +11 -0
  18. package/dist/cjs/commands/copyuid-parser.js +32 -0
  19. package/dist/cjs/commands/create.d.ts +11 -0
  20. package/dist/cjs/commands/create.js +80 -0
  21. package/dist/cjs/commands/delete.d.ts +11 -0
  22. package/dist/cjs/commands/delete.js +40 -0
  23. package/dist/cjs/commands/enable.d.ts +9 -0
  24. package/dist/cjs/commands/enable.js +61 -0
  25. package/dist/cjs/commands/esearch-parser.d.ts +17 -0
  26. package/dist/cjs/commands/esearch-parser.js +91 -0
  27. package/dist/cjs/commands/expunge.d.ts +12 -0
  28. package/dist/cjs/commands/expunge.js +60 -0
  29. package/dist/cjs/commands/fetch.d.ts +30 -0
  30. package/dist/cjs/commands/fetch.js +241 -0
  31. package/dist/cjs/commands/id.d.ts +10 -0
  32. package/dist/cjs/commands/id.js +80 -0
  33. package/dist/cjs/commands/idle.d.ts +9 -0
  34. package/dist/cjs/commands/idle.js +347 -0
  35. package/dist/cjs/commands/list.d.ts +16 -0
  36. package/dist/cjs/commands/list.js +518 -0
  37. package/dist/cjs/commands/login.d.ts +11 -0
  38. package/dist/cjs/commands/login.js +42 -0
  39. package/dist/cjs/commands/logout.d.ts +8 -0
  40. package/dist/cjs/commands/logout.js +47 -0
  41. package/dist/cjs/commands/move.d.ts +13 -0
  42. package/dist/cjs/commands/move.js +57 -0
  43. package/dist/cjs/commands/namespace.d.ts +25 -0
  44. package/dist/cjs/commands/namespace.js +139 -0
  45. package/dist/cjs/commands/noop.d.ts +8 -0
  46. package/dist/cjs/commands/noop.js +22 -0
  47. package/dist/cjs/commands/quota.d.ts +10 -0
  48. package/dist/cjs/commands/quota.js +119 -0
  49. package/dist/cjs/commands/rename.d.ts +12 -0
  50. package/dist/cjs/commands/rename.js +48 -0
  51. package/dist/cjs/commands/search.d.ts +15 -0
  52. package/dist/cjs/commands/search.js +228 -0
  53. package/dist/cjs/commands/select.d.ts +25 -0
  54. package/dist/cjs/commands/select.js +250 -0
  55. package/dist/cjs/commands/starttls.d.ts +8 -0
  56. package/dist/cjs/commands/starttls.js +30 -0
  57. package/dist/cjs/commands/status-fields.d.ts +14 -0
  58. package/dist/cjs/commands/status-fields.js +61 -0
  59. package/dist/cjs/commands/status.d.ts +12 -0
  60. package/dist/cjs/commands/status.js +108 -0
  61. package/dist/cjs/commands/store.d.ts +19 -0
  62. package/dist/cjs/commands/store.js +93 -0
  63. package/dist/cjs/commands/subscribe.d.ts +9 -0
  64. package/dist/cjs/commands/subscribe.js +31 -0
  65. package/dist/cjs/commands/unsubscribe.d.ts +9 -0
  66. package/dist/cjs/commands/unsubscribe.js +31 -0
  67. package/dist/cjs/connection-deadline.d.ts +49 -0
  68. package/dist/cjs/connection-deadline.js +91 -0
  69. package/dist/cjs/errors.d.ts +83 -0
  70. package/dist/cjs/errors.js +13 -0
  71. package/dist/cjs/handler/imap-compiler.d.ts +24 -0
  72. package/dist/cjs/handler/imap-compiler.js +285 -0
  73. package/dist/cjs/handler/imap-formal-syntax.d.ts +28 -0
  74. package/dist/cjs/handler/imap-formal-syntax.js +121 -0
  75. package/dist/cjs/handler/imap-handler.d.ts +9 -0
  76. package/dist/cjs/handler/imap-handler.js +10 -0
  77. package/dist/cjs/handler/imap-parser.d.ts +16 -0
  78. package/dist/cjs/handler/imap-parser.js +90 -0
  79. package/dist/cjs/handler/imap-stream.d.ts +181 -0
  80. package/dist/cjs/handler/imap-stream.js +446 -0
  81. package/dist/cjs/handler/limits.d.ts +25 -0
  82. package/dist/cjs/handler/limits.js +51 -0
  83. package/dist/cjs/handler/parser-instance.d.ts +68 -0
  84. package/dist/cjs/handler/parser-instance.js +223 -0
  85. package/dist/cjs/handler/token-parser.d.ts +91 -0
  86. package/dist/cjs/handler/token-parser.js +673 -0
  87. package/dist/cjs/handler/types.d.ts +91 -0
  88. package/dist/cjs/handler/types.js +4 -0
  89. package/dist/cjs/imap-commands.d.ts +16 -0
  90. package/dist/cjs/imap-commands.js +74 -0
  91. package/dist/cjs/imap-flow.d.ts +676 -0
  92. package/dist/cjs/imap-flow.js +3949 -0
  93. package/dist/cjs/jp-decoder.d.ts +12 -0
  94. package/dist/cjs/jp-decoder.js +79 -0
  95. package/dist/cjs/limited-passthrough.d.ts +25 -0
  96. package/dist/cjs/limited-passthrough.js +54 -0
  97. package/dist/cjs/logger.d.ts +3 -0
  98. package/dist/cjs/logger.js +11 -0
  99. package/dist/cjs/package-info.d.ts +3 -0
  100. package/dist/cjs/package-info.js +7 -0
  101. package/dist/cjs/package.json +3 -0
  102. package/dist/cjs/proxy-connection.d.ts +33 -0
  103. package/dist/cjs/proxy-connection.js +392 -0
  104. package/dist/cjs/search-compiler.d.ts +34 -0
  105. package/dist/cjs/search-compiler.js +476 -0
  106. package/dist/cjs/special-use.d.ts +22 -0
  107. package/dist/cjs/special-use.js +911 -0
  108. package/dist/cjs/tools.d.ts +427 -0
  109. package/dist/cjs/tools.js +1496 -0
  110. package/{lib/imap-flow.d.ts → dist/cjs/types.d.ts} +386 -516
  111. package/dist/cjs/types.js +5 -0
  112. package/dist/esm/charsets.d.ts +1 -0
  113. package/{lib → dist/esm}/charsets.js +1 -6
  114. package/dist/esm/commands/append.d.ts +22 -0
  115. package/{lib → dist/esm}/commands/append.js +22 -52
  116. package/dist/esm/commands/authenticate.d.ts +24 -0
  117. package/{lib → dist/esm}/commands/authenticate.js +62 -87
  118. package/dist/esm/commands/capability.d.ts +8 -0
  119. package/{lib → dist/esm}/commands/capability.js +6 -9
  120. package/dist/esm/commands/close.d.ts +8 -0
  121. package/{lib → dist/esm}/commands/close.js +6 -10
  122. package/dist/esm/commands/compress.d.ts +8 -0
  123. package/{lib → dist/esm}/commands/compress.js +7 -11
  124. package/dist/esm/commands/copy.d.ts +13 -0
  125. package/{lib → dist/esm}/commands/copy.js +12 -20
  126. package/dist/esm/commands/copyuid-parser.d.ts +11 -0
  127. package/{lib → dist/esm}/commands/copyuid-parser.js +9 -15
  128. package/dist/esm/commands/create.d.ts +11 -0
  129. package/{lib → dist/esm}/commands/create.js +13 -27
  130. package/dist/esm/commands/delete.d.ts +11 -0
  131. package/{lib → dist/esm}/commands/delete.js +9 -14
  132. package/dist/esm/commands/enable.d.ts +9 -0
  133. package/{lib → dist/esm}/commands/enable.js +23 -30
  134. package/dist/esm/commands/esearch-parser.d.ts +17 -0
  135. package/dist/esm/commands/esearch-parser.js +88 -0
  136. package/dist/esm/commands/expunge.d.ts +12 -0
  137. package/{lib → dist/esm}/commands/expunge.js +17 -22
  138. package/dist/esm/commands/fetch.d.ts +30 -0
  139. package/{lib → dist/esm}/commands/fetch.js +32 -64
  140. package/dist/esm/commands/id.d.ts +10 -0
  141. package/{lib → dist/esm}/commands/id.js +17 -23
  142. package/dist/esm/commands/idle.d.ts +9 -0
  143. package/{lib → dist/esm}/commands/idle.js +47 -81
  144. package/dist/esm/commands/list.d.ts +16 -0
  145. package/{lib → dist/esm}/commands/list.js +56 -121
  146. package/dist/esm/commands/login.d.ts +11 -0
  147. package/{lib → dist/esm}/commands/login.js +10 -15
  148. package/dist/esm/commands/logout.d.ts +8 -0
  149. package/{lib → dist/esm}/commands/logout.js +9 -11
  150. package/dist/esm/commands/move.d.ts +13 -0
  151. package/{lib → dist/esm}/commands/move.js +13 -21
  152. package/dist/esm/commands/namespace.d.ts +25 -0
  153. package/{lib → dist/esm}/commands/namespace.js +34 -44
  154. package/dist/esm/commands/noop.d.ts +8 -0
  155. package/{lib → dist/esm}/commands/noop.js +6 -7
  156. package/dist/esm/commands/quota.d.ts +10 -0
  157. package/{lib → dist/esm}/commands/quota.js +18 -36
  158. package/dist/esm/commands/rename.d.ts +12 -0
  159. package/{lib → dist/esm}/commands/rename.js +10 -15
  160. package/dist/esm/commands/search.d.ts +15 -0
  161. package/{lib → dist/esm}/commands/search.js +36 -135
  162. package/dist/esm/commands/select.d.ts +25 -0
  163. package/{lib → dist/esm}/commands/select.js +33 -64
  164. package/dist/esm/commands/starttls.d.ts +8 -0
  165. package/{lib → dist/esm}/commands/starttls.js +6 -8
  166. package/dist/esm/commands/status-fields.d.ts +14 -0
  167. package/{lib → dist/esm}/commands/status-fields.js +5 -16
  168. package/dist/esm/commands/status.d.ts +12 -0
  169. package/{lib → dist/esm}/commands/status.js +18 -29
  170. package/dist/esm/commands/store.d.ts +19 -0
  171. package/{lib → dist/esm}/commands/store.js +24 -37
  172. package/dist/esm/commands/subscribe.d.ts +9 -0
  173. package/{lib → dist/esm}/commands/subscribe.js +8 -12
  174. package/dist/esm/commands/unsubscribe.d.ts +9 -0
  175. package/{lib → dist/esm}/commands/unsubscribe.js +8 -12
  176. package/dist/esm/connection-deadline.d.ts +49 -0
  177. package/{lib → dist/esm}/connection-deadline.js +14 -25
  178. package/dist/esm/errors.d.ts +83 -0
  179. package/dist/esm/errors.js +9 -0
  180. package/dist/esm/handler/imap-compiler.d.ts +24 -0
  181. package/{lib → dist/esm}/handler/imap-compiler.js +22 -80
  182. package/dist/esm/handler/imap-formal-syntax.d.ts +28 -0
  183. package/dist/esm/handler/imap-formal-syntax.js +117 -0
  184. package/dist/esm/handler/imap-handler.d.ts +9 -0
  185. package/dist/esm/handler/imap-handler.js +9 -0
  186. package/dist/esm/handler/imap-parser.d.ts +16 -0
  187. package/{lib → dist/esm}/handler/imap-parser.js +31 -44
  188. package/dist/esm/handler/imap-stream.d.ts +181 -0
  189. package/{lib → dist/esm}/handler/imap-stream.js +29 -121
  190. package/dist/esm/handler/limits.d.ts +25 -0
  191. package/{lib → dist/esm}/handler/limits.js +13 -22
  192. package/dist/esm/handler/parser-instance.d.ts +68 -0
  193. package/{lib → dist/esm}/handler/parser-instance.js +19 -47
  194. package/dist/esm/handler/token-parser.d.ts +91 -0
  195. package/{lib → dist/esm}/handler/token-parser.js +71 -155
  196. package/dist/esm/handler/types.d.ts +91 -0
  197. package/dist/esm/handler/types.js +3 -0
  198. package/dist/esm/imap-commands.d.ts +16 -0
  199. package/dist/esm/imap-commands.js +67 -0
  200. package/dist/esm/imap-flow.d.ts +676 -0
  201. package/{lib → dist/esm}/imap-flow.js +785 -1802
  202. package/dist/esm/jp-decoder.d.ts +12 -0
  203. package/{lib → dist/esm}/jp-decoder.js +6 -21
  204. package/dist/esm/limited-passthrough.d.ts +25 -0
  205. package/{lib → dist/esm}/limited-passthrough.js +7 -20
  206. package/dist/esm/logger.d.ts +3 -0
  207. package/dist/esm/logger.js +4 -0
  208. package/dist/esm/package-info.d.ts +3 -0
  209. package/dist/esm/package-info.js +4 -0
  210. package/dist/esm/package.json +3 -0
  211. package/dist/esm/proxy-connection.d.ts +33 -0
  212. package/{lib → dist/esm}/proxy-connection.js +56 -127
  213. package/dist/esm/search-compiler.d.ts +34 -0
  214. package/{lib → dist/esm}/search-compiler.js +54 -110
  215. package/dist/esm/special-use.d.ts +22 -0
  216. package/dist/esm/special-use.js +907 -0
  217. package/dist/esm/tools.d.ts +427 -0
  218. package/dist/esm/tools.js +1446 -0
  219. package/dist/esm/types.d.ts +828 -0
  220. package/dist/esm/types.js +4 -0
  221. package/package.json +60 -20
  222. package/.gitattributes +0 -1
  223. package/.github/CODE_OF_CONDUCT.md +0 -76
  224. package/.github/FUNDING.yml +0 -4
  225. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  226. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  227. package/.github/contributing.md +0 -17
  228. package/.github/workflows/release.yaml +0 -36
  229. package/.github/workflows/stale.yml +0 -29
  230. package/.github/workflows/test.yml +0 -51
  231. package/.ncurc.js +0 -4
  232. package/.prettierignore +0 -4
  233. package/.prettierrc.js +0 -8
  234. package/.release-please-manifest.json +0 -3
  235. package/CLAUDE.md +0 -104
  236. package/Gruntfile.js +0 -23
  237. package/eslint.config.js +0 -45
  238. package/lib/handler/imap-formal-syntax.js +0 -189
  239. package/lib/handler/imap-handler.js +0 -17
  240. package/lib/imap-commands.js +0 -45
  241. package/lib/logger.js +0 -5
  242. package/lib/special-use.js +0 -923
  243. package/lib/tools.js +0 -1612
  244. package/release-please-config.json +0 -10
  245. package/test/authentication-test.js +0 -101
  246. package/test/auto-idle-test.js +0 -470
  247. package/test/bodystructure-test.js +0 -899
  248. package/test/charsets-test.js +0 -161
  249. package/test/commands-branches-test.js +0 -1095
  250. package/test/commands-integration-test.js +0 -11124
  251. package/test/commands-test.js +0 -73
  252. package/test/connection-edge-cases-test.js +0 -1828
  253. package/test/connection-test.js +0 -162
  254. package/test/copyuid-parser-test.js +0 -173
  255. package/test/fetch-generator-test.js +0 -218
  256. package/test/fixtures/fake-timers.js +0 -115
  257. package/test/fixtures/serialized-mimetorture.js +0 -2738
  258. package/test/fixtures/test-client.js +0 -57
  259. package/test/fixtures/test-tls.js +0 -8
  260. package/test/handler-branches-test.js +0 -310
  261. package/test/idle-polling-test.js +0 -518
  262. package/test/imap-compiler-test.js +0 -809
  263. package/test/imap-flow-compress-test.js +0 -166
  264. package/test/imap-flow-coverage-test.js +0 -612
  265. package/test/imap-flow-fetch-download-test.js +0 -873
  266. package/test/imap-flow-internals-test.js +0 -725
  267. package/test/imap-flow-methods-test.js +0 -889
  268. package/test/imap-flow-proxy-paths-test.js +0 -366
  269. package/test/imap-flow-secure-test.js +0 -573
  270. package/test/imap-flow-server-test.js +0 -1474
  271. package/test/imap-formal-syntax-test.js +0 -293
  272. package/test/imap-parser-test.js +0 -1474
  273. package/test/imap-stream-edge-cases-test.js +0 -666
  274. package/test/imap-stream-test.js +0 -177
  275. package/test/imapflow-test.js +0 -258
  276. package/test/integration/README.md +0 -52
  277. package/test/integration/dovecot-test.conf +0 -27
  278. package/test/integration/rev2-live-test.js +0 -431
  279. package/test/integration/run-rev2-tests.sh +0 -75
  280. package/test/integration-test.js +0 -83
  281. package/test/jp-decoder-test.js +0 -304
  282. package/test/limited-passthrough-test.js +0 -299
  283. package/test/memory-cleanup-test.js +0 -144
  284. package/test/memory-leak-test.js +0 -667
  285. package/test/parser-limits-test.js +0 -292
  286. package/test/proxy-connection-test.js +0 -738
  287. package/test/reliability-improvements-test.js +0 -548
  288. package/test/search-compiler-test.js +0 -1300
  289. package/test/search-test.js +0 -329
  290. package/test/special-use-test.js +0 -418
  291. package/test/starttls-injection-test.js +0 -181
  292. package/test/tag-correlation-test.js +0 -333
  293. package/test/timer-policy-test.js +0 -227
  294. package/test/token-parser-test.js +0 -456
  295. package/test/tools-test.js +0 -2013
  296. package/test/unhandled-rejection-test.js +0 -593
@@ -1,666 +0,0 @@
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['LiteralTooLarge error honors configured maxLiteralSize'] = test => {
135
- const cap = 1024; // 1KB cap
136
- const stream = new ImapStream({ cid: 'test', maxLiteralSize: cap });
137
-
138
- stream.on('error', err => {
139
- test.equal(err.code, 'LiteralTooLarge', 'error code should be LiteralTooLarge');
140
- test.equal(err.maxSize, cap, 'maxSize should reflect the configured cap');
141
- test.equal(err.literalSize, 2048, 'literalSize should be the offending value');
142
- stream.destroy();
143
- test.done();
144
- });
145
-
146
- stream.write(Buffer.from('A APPEND {2048}\r\n'));
147
- };
148
-
149
- module.exports['maxLiteralSize: 0 is honored (not swallowed into the default)'] = test => {
150
- // Regression: `this.options.maxLiteralSize || MAX_LITERAL_SIZE` turned an explicit 0 into
151
- // the 1GB default. An explicit 0 must mean "reject any non-empty literal".
152
- test.expect(2);
153
-
154
- const stream = new ImapStream({ cid: 'test', maxLiteralSize: 0 });
155
- test.equal(stream.maxLiteralSize, 0, 'an explicit 0 cap is preserved, not replaced by the default');
156
-
157
- stream.on('error', err => {
158
- test.equal(err.code, 'LiteralTooLarge', 'a 1-byte literal exceeds the 0 cap');
159
- stream.destroy();
160
- test.done();
161
- });
162
-
163
- stream.write(Buffer.from('A APPEND {1}\r\n'));
164
- };
165
-
166
- module.exports['Literal within configured maxLiteralSize parses cleanly'] = test => {
167
- // Require both literal assertions to actually run: 'end' fires even if the parser never
168
- // emits the command, so without expect() a dropped-literal regression would pass green.
169
- test.expect(2);
170
-
171
- const stream = new ImapStream({ cid: 'test', maxLiteralSize: 1024 });
172
- const literal = Buffer.alloc(512, 0x61); // 512 * 'a'
173
-
174
- stream.on('readable', () => {
175
- let cmd;
176
- while ((cmd = stream.read()) !== null) {
177
- test.equal(cmd.literals.length, 1, 'should have one literal');
178
- test.equal(cmd.literals[0].length, 512, 'literal length should be 512');
179
- cmd.next();
180
- }
181
- });
182
-
183
- stream.on('error', err => test.ifError(err));
184
-
185
- stream.on('end', () => test.done());
186
-
187
- stream.write(Buffer.from('A APPEND {512}\r\n'));
188
- stream.write(literal);
189
- stream.end(Buffer.from('\r\n'));
190
- };
191
-
192
- module.exports['Incomplete line continued in next chunk'] = test => {
193
- runStreamTest(
194
- test,
195
- cmd => {
196
- test.equal(cmd.payload.toString(), 'A CAPABILITY', 'payload should be A CAPABILITY');
197
- test.equal(cmd.literals.length, 0, 'should have no literals');
198
- },
199
- async stream => {
200
- stream.write(Buffer.from('A CA'));
201
- stream.end(Buffer.from('PABILITY\r\n'));
202
- },
203
- 1
204
- );
205
- };
206
-
207
- module.exports['Empty chunk then valid command'] = test => {
208
- runStreamTest(
209
- test,
210
- cmd => {
211
- test.equal(cmd.payload.toString(), 'A CMD', 'payload should be A CMD');
212
- test.equal(cmd.literals.length, 0, 'should have no literals');
213
- },
214
- async stream => {
215
- stream.write(Buffer.alloc(0));
216
- stream.end(Buffer.from('A CMD\r\n'));
217
- },
218
- 1
219
- );
220
- };
221
-
222
- module.exports['String input converted to Buffer'] = test => {
223
- runStreamTest(
224
- test,
225
- cmd => {
226
- test.equal(cmd.payload.toString(), 'A CMD', 'payload should be A CMD');
227
- test.ok(Buffer.isBuffer(cmd.payload), 'payload should be a Buffer');
228
- test.equal(cmd.literals.length, 0, 'should have no literals');
229
- },
230
- async stream => {
231
- stream.end('A CMD\r\n');
232
- },
233
- 1
234
- );
235
- };
236
-
237
- module.exports['LF-only line terminator'] = test => {
238
- runStreamTest(
239
- test,
240
- cmd => {
241
- test.equal(cmd.payload.toString(), 'A CMD', 'payload should be A CMD without CR or LF');
242
- test.equal(cmd.literals.length, 0, 'should have no literals');
243
- },
244
- async stream => {
245
- stream.end(Buffer.from('A CMD\n'));
246
- },
247
- 1
248
- );
249
- };
250
-
251
- module.exports['Many chunks trigger event loop yield'] = test => {
252
- runStreamTest(
253
- test,
254
- cmd => {
255
- // Just verify each command is valid
256
- test.ok(cmd.payload.toString().startsWith('A'), 'command should start with tag');
257
- },
258
- async stream => {
259
- for (let i = 0; i < 15; i++) {
260
- stream.write(Buffer.from(`A CMD${i}\r\n`));
261
- }
262
- stream.end();
263
- },
264
- 15
265
- );
266
- };
267
-
268
- module.exports['Destroy with queued items does not hang'] = test => {
269
- const stream = new ImapStream({ cid: 'test' });
270
- let errorEmitted = false;
271
-
272
- stream.on('error', () => {
273
- errorEmitted = true;
274
- });
275
-
276
- // Write multiple chunks rapidly then destroy
277
- stream.write(Buffer.from('A CMD1\r\n'));
278
- stream.write(Buffer.from('B CMD2\r\n'));
279
- stream.destroy();
280
-
281
- // Errors from destroy are emitted synchronously or on next tick
282
- setImmediate(() => {
283
- test.ok(!errorEmitted, 'should not emit error on destroy');
284
- test.done();
285
- });
286
- };
287
-
288
- module.exports['logRaw option triggers trace logging'] = test => {
289
- let traceCalled = false;
290
- let traceData = null;
291
-
292
- const stream = new ImapStream({
293
- cid: 'test',
294
- logRaw: true
295
- });
296
-
297
- // Override the log object to capture trace calls
298
- stream.log = {
299
- trace: data => {
300
- traceCalled = true;
301
- traceData = data;
302
- },
303
- debug: () => {},
304
- info: () => {},
305
- warn: () => {},
306
- error: () => {}
307
- };
308
-
309
- // Also need to handle readable events
310
- stream.on('readable', () => {
311
- let cmd;
312
- while ((cmd = stream.read()) !== null) {
313
- cmd.next();
314
- }
315
- });
316
-
317
- stream.on('end', () => {
318
- test.ok(traceCalled, 'trace should have been called');
319
- test.ok(traceData, 'trace data should exist');
320
- test.equal(traceData.src, 's', 'source should be s');
321
- test.ok(traceData.data, 'should have base64 data');
322
- test.done();
323
- });
324
-
325
- stream.end(Buffer.from('A CMD\r\n'));
326
- };
327
-
328
- module.exports['Adjacent literals with marker at line start'] = test => {
329
- // After the first literal's data (12345) is consumed, parsing resumes at the very start of
330
- // a line that is itself a literal marker ({3}). The marker begins at byte 0 of the resumed
331
- // line, which the backward scan must still recognize. Previously the loop bound skipped
332
- // index 0, so the second literal was silently dropped.
333
- runStreamTest(
334
- test,
335
- cmd => {
336
- test.equal(cmd.literals.length, 2, 'both adjacent literals must be extracted');
337
- test.equal(cmd.literals[0].toString(), '12345', 'first literal content');
338
- test.equal(cmd.literals[1].toString(), 'ABC', 'second literal content');
339
- },
340
- async stream => {
341
- stream.end(Buffer.from('A LOGIN {5}\r\n12345{3}\r\nABC\r\n'));
342
- },
343
- 1
344
- );
345
- };
346
-
347
- module.exports['Line length cap rejects oversized line'] = test => {
348
- // A server that never sends a line terminator must not grow the line buffer without bound.
349
- const stream = new ImapStream({ cid: 'test', maxLineLength: 16 });
350
- let errored = false;
351
-
352
- stream.on('error', err => {
353
- errored = true;
354
- test.equal(err.code, 'LineTooLarge', 'error code should be LineTooLarge');
355
- test.equal(err.maxSize, 16, 'error should report the configured cap');
356
- stream.destroy();
357
- test.done();
358
- });
359
-
360
- stream.on('end', () => {
361
- if (!errored) {
362
- test.ok(false, 'expected a LineTooLarge error');
363
- test.done();
364
- }
365
- });
366
-
367
- // 24 bytes, no LF, written across chunks -> exceeds the 16 byte cap.
368
- stream.write(Buffer.from('AAAAAAAA'));
369
- stream.write(Buffer.from('BBBBBBBB'));
370
- stream.write(Buffer.from('CCCCCCCC'));
371
- };
372
-
373
- module.exports['Line length cap allows line within limit'] = test => {
374
- // A normal line under the configured cap must still parse cleanly.
375
- const stream = new ImapStream({ cid: 'test', maxLineLength: 32 });
376
- let payloads = [];
377
-
378
- stream.on('readable', () => {
379
- let cmd;
380
- while ((cmd = stream.read()) !== null) {
381
- payloads.push(cmd.payload.toString());
382
- cmd.next();
383
- }
384
- });
385
-
386
- stream.on('error', err => test.ifError(err));
387
-
388
- stream.on('end', () => {
389
- test.deepEqual(payloads, ['A NOOP'], 'line under the cap should parse');
390
- test.done();
391
- });
392
-
393
- stream.end(Buffer.from('A NOOP\r\n'));
394
- };
395
-
396
- // ---------------------------------------------------------------------------
397
- // checkLiteralMarker direct edge cases + _transform string handling + _destroy
398
- // draining (lines that the normal streaming flow does not exercise).
399
- // ---------------------------------------------------------------------------
400
-
401
- module.exports['ImapStream: checkLiteralMarker returns false for empty line'] = test => {
402
- const stream = new ImapStream({ cid: 't' });
403
- test.equal(stream.checkLiteralMarker(Buffer.alloc(0)), false);
404
- test.equal(stream.checkLiteralMarker(null), false);
405
- test.done();
406
- };
407
-
408
- module.exports['ImapStream: checkLiteralMarker returns false when no trailing LF'] = test => {
409
- const stream = new ImapStream({ cid: 't' });
410
- test.equal(stream.checkLiteralMarker(Buffer.from('A1 OK no newline')), false);
411
- test.done();
412
- };
413
-
414
- module.exports['ImapStream: checkLiteralMarker returns false for non-numeric marker'] = test => {
415
- const stream = new ImapStream({ cid: 't' });
416
- // '{' present but contains a non-digit, and an empty {} marker
417
- test.equal(stream.checkLiteralMarker(Buffer.from('A1 CMD {x}\r\n')), false);
418
- test.equal(stream.checkLiteralMarker(Buffer.from('A1 CMD {}\r\n')), false);
419
- test.done();
420
- };
421
-
422
- module.exports['ImapStream: checkLiteralMarker activates literal state for valid marker'] = test => {
423
- const stream = new ImapStream({ cid: 't' });
424
- test.equal(stream.checkLiteralMarker(Buffer.from('A1 CMD {5}\r\n')), true);
425
- test.equal(stream.literalWaiting, 5);
426
- test.done();
427
- };
428
-
429
- module.exports['ImapStream: _transform converts string chunks to Buffer'] = test => {
430
- const stream = new ImapStream({ cid: 't' });
431
- let commands = [];
432
- stream.on('readable', () => {
433
- let cmd;
434
- while ((cmd = stream.read()) !== null) {
435
- commands.push(cmd.payload.toString());
436
- cmd.next();
437
- }
438
- });
439
- stream.on('end', () => {
440
- test.ok(commands.some(c => /A1 OK/.test(c)));
441
- test.done();
442
- });
443
- // write a string (not a Buffer) to exercise the string->Buffer branch
444
- stream.write('A1 OK done\r\n');
445
- stream.end();
446
- };
447
-
448
- module.exports['ImapStream: _destroy drains pending input queue callbacks'] = test => {
449
- const stream = new ImapStream({ cid: 't' });
450
- let nextCalled = false;
451
- // Stage a pending queue item with a next() callback, then destroy.
452
- stream.inputQueue.push({ chunk: Buffer.from('x'), next: () => (nextCalled = true) });
453
- stream.destroy();
454
- setImmediate(() => {
455
- test.ok(nextCalled, 'pending next() invoked during destroy');
456
- test.done();
457
- });
458
- };
459
-
460
- module.exports['Literal marker scan stays linear on a long digit run'] = test => {
461
- // A backwards scan that accumulated digits one at a time cost O(n^2), so a few
462
- // hundred KB of digits blocked the event loop for seconds before the size was
463
- // even known - all inside the default line-length budget
464
- const stream = new ImapStream({ cid: 'test' });
465
- stream.on('error', () => {});
466
- stream.resume();
467
-
468
- const line = Buffer.concat([Buffer.from('* OK {'), Buffer.from('9'.repeat(200000)), Buffer.from('}\r\n')]);
469
-
470
- const started = process.hrtime.bigint();
471
- stream.checkLiteralMarker(line);
472
- const elapsedMs = Number(process.hrtime.bigint() - started) / 1e6;
473
-
474
- test.ok(elapsedMs < 250, `scan should stay cheap, took ${elapsedMs.toFixed(1)}ms`);
475
- test.done();
476
- };
477
-
478
- module.exports['Literal marker fails the stream on an oversized digit run'] = test => {
479
- // An impossible size is still a syntactically valid marker. Treating it as an
480
- // ordinary line instead would feed the announced literal body to the line parser
481
- // and desynchronize the session, so the stream must end with LiteralTooLarge.
482
- const stream = new ImapStream({ cid: 'test' });
483
- let streamErr = null;
484
- stream.on('error', err => {
485
- streamErr = err;
486
- });
487
- stream.resume();
488
-
489
- const line = Buffer.concat([Buffer.from('* OK {'), Buffer.from('1'.repeat(40)), Buffer.from('}\r\n')]);
490
-
491
- test.equal(stream.checkLiteralMarker(line), false, 'an impossible size must not start literal mode');
492
- test.ok(stream.destroyed, 'the stream must fail closed instead of continuing as if the marker were text');
493
- setImmediate(() => {
494
- test.ok(streamErr && streamErr.code === 'LiteralTooLarge', 'must fail with LiteralTooLarge');
495
- test.done();
496
- });
497
- };
498
-
499
- module.exports['Literal marker accepts a zero-padded size'] = test => {
500
- // The RFC "number" production is 1*DIGIT, so leading zeros are legal - a long
501
- // digit run can still denote a small size and must be consumed as a literal
502
- const stream = new ImapStream({ cid: 'test' });
503
- stream.on('error', () => {});
504
- stream.resume();
505
-
506
- const line = Buffer.from(`* 1 FETCH (BODY[] {${'0'.repeat(21)}123}\r\n`);
507
-
508
- test.equal(stream.checkLiteralMarker(line), true, 'a zero-padded marker is a valid literal marker');
509
- test.equal(stream.literalWaiting, 123, 'the padded size must parse to its numeric value');
510
- stream.destroy();
511
- test.done();
512
- };
513
-
514
- module.exports['Literal marker still accepts sizes at the digit-length bound'] = test => {
515
- const stream = new ImapStream({ cid: 'test', maxLiteralSize: Number.MAX_SAFE_INTEGER });
516
- stream.on('error', () => {});
517
- stream.resume();
518
-
519
- // 19 digits is the widest a number64 gets, so it must still be recognized
520
- const line = Buffer.from(`* OK {${'9'.repeat(19)}}\r\n`);
521
-
522
- test.equal(stream.checkLiteralMarker(line), false, 'a size beyond the configured maximum fails the stream rather than parsing');
523
-
524
- const ok = new ImapStream({ cid: 'test' });
525
- ok.on('error', () => {});
526
- ok.resume();
527
- test.equal(ok.checkLiteralMarker(Buffer.from('* OK {1024}\r\n')), true);
528
- test.equal(ok.literalWaiting, 1024);
529
- test.done();
530
- };
531
-
532
- module.exports['Response assembly enforces the cumulative size cap'] = test => {
533
- // The per-line and per-literal caps alone cannot stop a response spread across many
534
- // tokens: under a 40-byte response budget, a 25-byte marker line plus a declared
535
- // 10-byte literal fits (35), but the next marker line (16 bytes + 10 declared) must
536
- // trip the cap - before the second literal's bytes are even read
537
- const stream = new ImapStream({ cid: 'test', maxResponseSize: 40 });
538
- let streamErr = null;
539
- stream.on('error', err => {
540
- streamErr = err;
541
- });
542
- stream.resume();
543
-
544
- stream.write(Buffer.from('* 1 FETCH (BODY[1] {10}\r\n'));
545
- stream.write(Buffer.from('0123456789'));
546
- stream.write(Buffer.from(' BODY[2] {10}\r\n0123456789)\r\n'));
547
-
548
- setTimeout(() => {
549
- test.ok(streamErr, 'an oversized cumulative response must fail the stream');
550
- test.equal(streamErr && streamErr.code, 'ResponseTooLarge');
551
- test.ok(stream.destroyed, 'the stream must fail closed instead of parsing the rejected payload');
552
- test.done();
553
- }, 100);
554
- };
555
-
556
- // Resolves once the stream has settled - on its first 'error', or on 'end' when it is
557
- // consumed to completion. Waiting for the real signal keeps these tests off fixed sleeps.
558
- const settle = stream =>
559
- new Promise(resolve => {
560
- stream.once('error', err => resolve(err));
561
- stream.once('end', () => resolve(null));
562
- });
563
-
564
- module.exports['Response size budget resets between responses'] = async test => {
565
- // The counter tracks a single response, not the whole session. Each response here fits
566
- // the 40-byte budget on its own but the two together do not, so a counter that failed to
567
- // reset would trip the cap on the second one - which is what makes this test detect the
568
- // regression rather than merely pass alongside it.
569
- const line = '* OK ' + 'a'.repeat(23) + '\r\n'; // 30 bytes, two of them exceed the budget
570
- const stream = new ImapStream({ cid: 'test', maxResponseSize: 40 });
571
- let streamErr = null;
572
- let count = 0;
573
- stream.on('error', err => {
574
- streamErr = err;
575
- });
576
- stream.on('data', cmd => {
577
- count++;
578
- cmd.next();
579
- });
580
-
581
- test.ok(line.length <= 40 && line.length * 2 > 40, 'each response fits the budget, the pair does not');
582
-
583
- stream.write(Buffer.from(line + line));
584
- stream.end();
585
-
586
- await settle(stream);
587
- test.ifError(streamErr);
588
- test.equal(count, 2);
589
- test.done();
590
- };
591
-
592
- module.exports['Response size cap defaults above the literal cap'] = test => {
593
- // The response total also carries the literal marker line and the rest of the framing, so
594
- // a default equal to the literal cap would make a literal of exactly the maximum permitted
595
- // size impossible to receive
596
- const stream = new ImapStream({ cid: 'test' });
597
- test.equal(stream.maxResponseSize, 2 * 1024 * 1024 * 1024);
598
- test.ok(stream.maxResponseSize > stream.maxLiteralSize, 'the response cap must leave headroom above the literal cap');
599
- test.done();
600
- };
601
-
602
- module.exports['A literal of exactly maxLiteralSize is accepted when the response cap leaves headroom'] = async test => {
603
- const stream = new ImapStream({ cid: 'test', maxLiteralSize: 100, maxResponseSize: 200 });
604
- let streamErr = null;
605
- let received = null;
606
- stream.on('error', err => {
607
- streamErr = err;
608
- });
609
- stream.on('data', cmd => {
610
- received = cmd;
611
- cmd.next();
612
- });
613
-
614
- stream.write(Buffer.from('* 1 FETCH (BODY[] {100}\r\n'));
615
- stream.write(Buffer.from('x'.repeat(100)));
616
- stream.write(Buffer.from(')\r\n'));
617
- stream.end();
618
-
619
- await settle(stream);
620
- test.ifError(streamErr);
621
- test.ok(received, 'a literal at exactly the configured maximum must be delivered');
622
- test.equal(received.literals.length, 1);
623
- test.equal(received.literals[0].length, 100);
624
- test.done();
625
- };
626
-
627
- module.exports['An unterminated line is bounded by the response budget'] = async test => {
628
- // maxResponseSize is only committed when a line completes, so an in-progress line has to
629
- // be measured against the remaining budget separately - otherwise a response cap lowered
630
- // to bound parser memory buys nothing while a server streams a line that never ends
631
- const stream = new ImapStream({ cid: 'test', maxResponseSize: 64 });
632
- stream.resume();
633
-
634
- stream.write(Buffer.from('x'.repeat(1024))); // no line terminator anywhere
635
-
636
- let err = await settle(stream);
637
- test.ok(err, 'an unterminated line beyond the response budget must fail the stream');
638
- test.equal(err.code, 'ResponseTooLarge');
639
- test.ok(stream.lineBytes <= 64, 'no more than the budget may stay buffered');
640
- test.done();
641
- };
642
-
643
- module.exports['Infinity disables a parser size cap'] = test => {
644
- // A cap that cannot be disabled forces a caller who knows their server onto the default
645
- const stream = new ImapStream({ cid: 'test', maxResponseSize: Infinity, maxLiteralSize: Infinity, maxLineLength: Infinity });
646
- test.equal(stream.maxResponseSize, Infinity);
647
- test.equal(stream.maxLiteralSize, Infinity);
648
- test.equal(stream.maxLineLength, Infinity);
649
- test.done();
650
- };
651
-
652
- module.exports['A marker line that fits the budget can still be refused for its literal'] = async test => {
653
- // The line is measured against the budget as it is assembled, but the declared literal is
654
- // only charged once the marker line completes - so the cap has to be enforced in both places
655
- const stream = new ImapStream({ cid: 'test', maxResponseSize: 40 });
656
- stream.resume();
657
-
658
- // 25-byte marker line fits on its own; the 30 declared literal bytes push the total past 40
659
- stream.write(Buffer.from('* 1 FETCH (BODY[1] {30}\r\n'));
660
-
661
- let err = await settle(stream);
662
- test.ok(err, 'the declared literal must be charged before its bytes arrive');
663
- test.equal(err.code, 'ResponseTooLarge');
664
- test.equal(err.responseSize, 55);
665
- test.done();
666
- };