imapflow 1.7.8 → 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 +20 -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 +761 -1789
  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 -101
  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 -909
  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 -661
@@ -1,333 +0,0 @@
1
- 'use strict';
2
-
3
- // Tagged response correlation: a tagged response may only complete the command that was
4
- // actually written to the socket. A response for a queued-but-unsent command is proof of
5
- // desynchronization and fails the connection closed; a wholly unknown tag is recorded but
6
- // tolerated, because non-conforming servers do emit stray tagged lines.
7
-
8
- const net = require('net');
9
- const { ImapFlow } = require('../lib/imap-flow');
10
-
11
- const CAPABILITIES = 'IMAP4rev1 ID ENABLE NAMESPACE';
12
-
13
- // Minimal scriptable IMAP server. `onCommand(ctx)` may fully handle a command by returning
14
- // true; otherwise the default happy-path session responses are used.
15
- const createServer = onCommand =>
16
- net.createServer(socket => {
17
- socket.setNoDelay(true);
18
- socket.on('error', () => {});
19
- socket.write(`* OK [CAPABILITY ${CAPABILITIES}] mock ready\r\n`);
20
-
21
- let buf = '';
22
- socket.on('data', data => {
23
- buf += data.toString('binary');
24
- let idx;
25
- while ((idx = buf.indexOf('\r\n')) >= 0) {
26
- let line = buf.slice(0, idx);
27
- buf = buf.slice(idx + 2);
28
-
29
- let parts = line.split(' ');
30
- let ctx = {
31
- tag: parts[0],
32
- command: (parts[1] || '').toUpperCase(),
33
- socket,
34
- write: str => socket.write(str),
35
- ok: text => socket.write(`${parts[0]} OK ${text || 'completed'}\r\n`)
36
- };
37
-
38
- if (onCommand && onCommand(ctx)) {
39
- continue;
40
- }
41
-
42
- switch (ctx.command) {
43
- case 'CAPABILITY':
44
- ctx.write(`* CAPABILITY ${CAPABILITIES}\r\n`);
45
- ctx.ok();
46
- break;
47
- case 'NAMESPACE':
48
- ctx.write('* NAMESPACE (("" "/")) NIL NIL\r\n');
49
- ctx.ok();
50
- break;
51
- case 'ID':
52
- ctx.write('* ID ("name" "mock")\r\n');
53
- ctx.ok();
54
- break;
55
- default:
56
- ctx.ok();
57
- }
58
- }
59
- });
60
- });
61
-
62
- const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve(server.address().port)));
63
-
64
- // Captures warn/error entries so bounded logging can be asserted.
65
- const createLogger = () => {
66
- const entries = { warn: [], error: [] };
67
- const noop = () => {};
68
- return {
69
- trace: noop,
70
- debug: noop,
71
- info: noop,
72
- warn: entry => entries.warn.push(entry),
73
- error: entry => entries.error.push(entry),
74
- fatal: noop,
75
- entries
76
- };
77
- };
78
-
79
- const makeClient = (port, overrides = {}) =>
80
- new ImapFlow({
81
- host: '127.0.0.1',
82
- port,
83
- secure: false,
84
- disableAutoIdle: true,
85
- disableCompression: true,
86
- logger: false,
87
- auth: { user: 'test', pass: 'secret' },
88
- ...overrides
89
- });
90
-
91
- // Command tags are sequential hex counters, so a server can trivially guess the tag of a
92
- // command that has been queued but not yet sent.
93
- const nextTag = tag => (parseInt(tag, 16) + 1).toString(16).toUpperCase();
94
-
95
- module.exports['Tag correlation: response for a queued but unsent command closes the connection'] = async test => {
96
- let server = createServer(ctx => {
97
- if (ctx.command !== 'NOOP') {
98
- return false;
99
- }
100
- // Answer with the tag of the command still sitting in the queue
101
- ctx.write(`${nextTag(ctx.tag)} OK forged completion\r\n`);
102
- return true;
103
- });
104
- let port = await listen(server);
105
- let client = makeClient(port);
106
-
107
- let errors = [];
108
- client.on('error', err => errors.push(err));
109
-
110
- await client.connect();
111
-
112
- let first = client.exec('NOOP', false, {});
113
- let second = client.exec('NOOP', false, {});
114
-
115
- let firstErr = await first.then(() => null).catch(err => err);
116
- let secondErr = await second.then(() => null).catch(err => err);
117
-
118
- test.ok(secondErr, 'the queued command did not resolve from a response it never asked for');
119
- test.equal(secondErr.code, 'UnexpectedTag', 'the queued command rejects with the protocol error');
120
- test.ok(secondErr.details.received, 'the received tag is recorded');
121
- test.equal(secondErr.details.received, nextTag(secondErr.details.expected), 'the received tag is the queued (unsent) tag');
122
-
123
- test.ok(firstErr, 'the in-flight command also rejects rather than hanging');
124
- test.ok(['UnexpectedTag', 'NoConnection'].includes(firstErr.code), `in-flight command rejected with ${firstErr.code}`);
125
-
126
- test.ok(
127
- errors.some(err => err.code === 'UnexpectedTag'),
128
- 'the desynchronization is reported to the caller'
129
- );
130
- test.ok(!client.usable, 'the connection is no longer usable');
131
-
132
- client.close();
133
- server.close();
134
- test.done();
135
- };
136
-
137
- module.exports['Tag correlation: unknown tag is counted and survived'] = async test => {
138
- let logger = createLogger();
139
- let server = createServer(ctx => {
140
- if (ctx.command !== 'NOOP') {
141
- return false;
142
- }
143
- ctx.write('ZZZZ OK stray response\r\n');
144
- ctx.ok('NOOP completed');
145
- return true;
146
- });
147
- let port = await listen(server);
148
- let client = makeClient(port, { logger });
149
-
150
- let errors = [];
151
- client.on('error', err => errors.push(err));
152
-
153
- await client.connect();
154
- test.equal(client._unknownTagCount, 0, 'a normal session issues no unknown tags');
155
-
156
- let response = await client.exec('NOOP', false, {});
157
- response.next();
158
-
159
- test.equal(client._unknownTagCount, 1, 'the stray tagged line is counted');
160
- test.ok(client.usable, 'the connection survives a stray tagged line');
161
- test.deepEqual(errors, [], 'no error is emitted for a merely unknown tag');
162
- test.ok(
163
- logger.entries.warn.some(entry => entry.msg === 'Tagged response for an unknown tag' && entry.tag === 'ZZZZ'),
164
- 'the stray tag is logged at warn level'
165
- );
166
-
167
- await client.logout();
168
- client.close();
169
- server.close();
170
- test.done();
171
- };
172
-
173
- module.exports['Tag correlation: unknown tag warnings are bounded but the count stays exact'] = async test => {
174
- const strayCount = 10;
175
- let logger = createLogger();
176
- let server = createServer(ctx => {
177
- if (ctx.command !== 'NOOP') {
178
- return false;
179
- }
180
- for (let i = 0; i < strayCount; i++) {
181
- ctx.write(`STRAY${i} OK stray response\r\n`);
182
- }
183
- ctx.ok('NOOP completed');
184
- return true;
185
- });
186
- let port = await listen(server);
187
- let client = makeClient(port, { logger });
188
- client.on('error', () => {});
189
-
190
- await client.connect();
191
- let response = await client.exec('NOOP', false, {});
192
- response.next();
193
-
194
- test.equal(client._unknownTagCount, strayCount, 'every stray tag is counted');
195
-
196
- let warnings = logger.entries.warn.filter(entry => entry.msg === 'Tagged response for an unknown tag');
197
- // Logged at 1, 2, 4 and 8 - the powers of two up to 10
198
- test.equal(warnings.length, 4, 'warning volume is bounded rather than one line per stray tag');
199
- test.deepEqual(
200
- warnings.map(entry => entry.unknownTagCount),
201
- [1, 2, 4, 8],
202
- 'warnings are emitted at the first occurrence and then at powers of two'
203
- );
204
-
205
- client.close();
206
- server.close();
207
- test.done();
208
- };
209
-
210
- module.exports['Tag correlation: commands stay serialized'] = async test => {
211
- let inFlight = 0;
212
- let maxInFlight = 0;
213
- let server = createServer(ctx => {
214
- if (ctx.command !== 'NOOP') {
215
- return false;
216
- }
217
- inFlight++;
218
- maxInFlight = Math.max(maxInFlight, inFlight);
219
- setTimeout(() => {
220
- inFlight--;
221
- ctx.ok('NOOP completed');
222
- }, 20);
223
- return true;
224
- });
225
- let port = await listen(server);
226
- let client = makeClient(port);
227
- client.on('error', () => {});
228
-
229
- await client.connect();
230
-
231
- // sequential
232
- for (let i = 0; i < 2; i++) {
233
- let response = await client.exec('NOOP', false, {});
234
- response.next();
235
- }
236
-
237
- // concurrently queued. Each caller releases its own response as soon as it settles, which
238
- // is the contract every command handler follows.
239
- await Promise.all(
240
- [0, 1, 2].map(async () => {
241
- let response = await client.exec('NOOP', false, {});
242
- response.next();
243
- })
244
- );
245
-
246
- test.equal(maxInFlight, 1, 'at most one command is active on the wire');
247
-
248
- client.close();
249
- server.close();
250
- test.done();
251
- };
252
-
253
- module.exports['Tag correlation: the next command is sent only after the previous handler returns'] = async test => {
254
- // Pins the completion ordering: a command handler applies its state (mailbox selection,
255
- // capabilities) between the tagged response and the next command reaching the wire.
256
- let received = [];
257
- let server = createServer(ctx => {
258
- if (ctx.command !== 'NOOP' && ctx.command !== 'CAPABILITY') {
259
- return false;
260
- }
261
- received.push(ctx.command);
262
- ctx.write(ctx.command === 'CAPABILITY' ? `* CAPABILITY ${CAPABILITIES}\r\n` : '');
263
- ctx.ok(`${ctx.command} completed`);
264
- return true;
265
- });
266
- let port = await listen(server);
267
- let client = makeClient(port);
268
- client.on('error', () => {});
269
-
270
- await client.connect();
271
- received = [];
272
-
273
- let first = client.exec('NOOP', false, {});
274
- let second = client.exec('CAPABILITY', false, {});
275
-
276
- let firstResponse = await first;
277
- test.deepEqual(received, ['NOOP'], 'the queued command is not on the wire while the handler still runs');
278
-
279
- firstResponse.next();
280
- let secondResponse = await second;
281
- secondResponse.next();
282
-
283
- test.deepEqual(received, ['NOOP', 'CAPABILITY'], 'the queued command is dispatched once the handler returned');
284
-
285
- client.close();
286
- server.close();
287
- test.done();
288
- };
289
-
290
- module.exports['Tag correlation: a response for a command that is not on the wire yet is desync'] = async test => {
291
- // The active command becomes `currentRequest` before it is written (compiling the command is
292
- // asynchronous). Tags are sequential and guessable, so a server that answers during that
293
- // window must not settle the command either.
294
- let server = createServer();
295
- let port = await listen(server);
296
- let client = makeClient(port);
297
-
298
- let errors = [];
299
- client.on('error', err => errors.push(err));
300
-
301
- await client.connect();
302
-
303
- let rejected = null;
304
- let request = {
305
- command: 'NOOP',
306
- attributes: [],
307
- options: {},
308
- resolve: () => test.ok(false, 'a command that was never written must not resolve'),
309
- reject: err => (rejected = err)
310
- };
311
- client.requestTagMap.set('A1', request);
312
- // current, but not marked as written
313
- client.currentRequest = { tag: 'A1', command: 'NOOP' };
314
-
315
- let served = false;
316
- client.streamer.read = () => {
317
- if (served) {
318
- return null;
319
- }
320
- served = true;
321
- return { payload: Buffer.from('A1 OK done'), literals: [], next: () => {} };
322
- };
323
-
324
- await client.reader();
325
-
326
- test.ok(rejected, 'the command is rejected instead of settled');
327
- test.equal(rejected.code, 'UnexpectedTag', 'reported as protocol desynchronization');
328
- test.equal(rejected.details.received, 'A1');
329
-
330
- client.close();
331
- server.close();
332
- test.done();
333
- };
@@ -1,227 +0,0 @@
1
- 'use strict';
2
-
3
- // Timer process-liveness policy:
4
- // * connection establishment and greeting deadlines keep the process alive, because a caller is
5
- // waiting for connect() to settle
6
- // * background timers (auto-IDLE, IDLE restart, fallback polling, throttle back-off) are unref'd
7
- // * every timer is cleared explicitly on close()
8
- //
9
- // Asserted through timer identity and cleanup rather than wall-clock sleeps.
10
-
11
- const net = require('net');
12
- const idleCommand = require('../lib/commands/idle.js');
13
- const { withFakeTimers } = require('./fixtures/fake-timers');
14
- const { makeClient, makeIdleReadyClient } = require('./fixtures/test-client');
15
-
16
- const CAPS = 'IMAP4rev1 ID ENABLE NAMESPACE IDLE';
17
-
18
- const createServer = () =>
19
- net.createServer(socket => {
20
- socket.setNoDelay(true);
21
- socket.on('error', () => {});
22
- let buf = '';
23
- socket.on('data', data => {
24
- buf += data.toString('binary');
25
- let idx;
26
- while ((idx = buf.indexOf('\r\n')) >= 0) {
27
- let line = buf.slice(0, idx);
28
- buf = buf.slice(idx + 2);
29
- let parts = line.split(' ');
30
- let tag = parts[0];
31
- let cmd = (parts[1] || '').toUpperCase();
32
- switch (cmd) {
33
- case 'CAPABILITY':
34
- socket.write(`* CAPABILITY ${CAPS}\r\n${tag} OK done\r\n`);
35
- break;
36
- case 'ID':
37
- socket.write(`* ID ("name" "mock")\r\n${tag} OK done\r\n`);
38
- break;
39
- case 'NAMESPACE':
40
- socket.write(`* NAMESPACE (("" "/")) NIL NIL\r\n${tag} OK done\r\n`);
41
- break;
42
- case 'LOGOUT':
43
- socket.write(`* BYE bye\r\n${tag} OK done\r\n`);
44
- break;
45
- default:
46
- socket.write(`${tag} OK ok\r\n`);
47
- }
48
- }
49
- });
50
- socket.write(`* OK [CAPABILITY ${CAPS}] ready\r\n`);
51
- });
52
-
53
- const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve(server.address().port)));
54
-
55
- module.exports['Timers: connection and greeting deadlines keep the process alive'] = async test => {
56
- let server = createServer();
57
- let port = await listen(server);
58
-
59
- await withFakeTimers(async timers => {
60
- let client = makeClient({
61
- port,
62
- secure: false,
63
- connectionTimeout: 12345,
64
- greetingTimeout: 6789,
65
- disableAutoIdle: true,
66
- disableCompression: true
67
- });
68
- client.on('error', () => {});
69
-
70
- await client.connect();
71
-
72
- let connectDeadline = timers.history().find(timer => timer.delay === 12345);
73
- let greetingDeadline = timers.history().find(timer => timer.delay === 6789);
74
-
75
- test.ok(connectDeadline, 'the connection deadline was armed');
76
- test.equal(connectDeadline.unrefd, false, 'the connection deadline keeps the process alive');
77
- test.ok(connectDeadline.cleared, 'and is cleared once the transport is established');
78
-
79
- test.ok(greetingDeadline, 'the greeting deadline was armed');
80
- test.equal(greetingDeadline.unrefd, false, 'the greeting deadline keeps the process alive');
81
- test.ok(greetingDeadline.cleared, 'and is cleared once the greeting arrives');
82
-
83
- client.close();
84
- test.equal(timers.count(), 0, 'no timer is left armed after close');
85
- });
86
-
87
- server.close();
88
- test.done();
89
- };
90
-
91
- module.exports['Timers: the auto-IDLE timer is unrefd and cleared on close'] = async test => {
92
- await withFakeTimers(async timers => {
93
- let client = makeIdleReadyClient();
94
-
95
- client.autoidle();
96
-
97
- let armed = timers.pending();
98
- test.equal(armed.length, 1, 'exactly one auto-IDLE timer is armed');
99
- test.equal(armed[0].delay, client.autoIdleDelay);
100
- test.ok(armed[0].unrefd, 'the background auto-IDLE timer does not keep the process alive');
101
-
102
- client.close();
103
- test.equal(timers.count(), 0, 'close() clears the auto-IDLE timer');
104
- test.ok(timers.history()[0].cleared, 'the timer was cleared, not just abandoned');
105
- });
106
- test.done();
107
- };
108
-
109
- module.exports['Timers: a restarted auto-IDLE timer replaces the previous one'] = async test => {
110
- await withFakeTimers(async timers => {
111
- let client = makeIdleReadyClient();
112
-
113
- client.autoidle();
114
- client.autoidle();
115
-
116
- test.equal(timers.count(), 1, 'only the newest auto-IDLE timer stays armed');
117
- test.ok(timers.history()[0].cleared, 'the superseded timer was cleared');
118
-
119
- client.close();
120
- test.done();
121
- });
122
- };
123
-
124
- module.exports['Timers: the IDLE restart timer is unrefd'] = async test => {
125
- await withFakeTimers(async timers => {
126
- const states = { NOT_AUTHENTICATED: 1, AUTHENTICATED: 2, SELECTED: 3, LOGOUT: 4 };
127
- let breakIdle;
128
-
129
- const connection = {
130
- states,
131
- state: states.SELECTED,
132
- id: 'timer-test',
133
- capabilities: new Map([['IDLE', true]]),
134
- enabled: new Set(),
135
- mailbox: { path: 'INBOX' },
136
- socket: { destroyed: false },
137
- idling: false,
138
- preCheck: false,
139
- log: { warn: () => {}, info: () => {}, error: () => {}, debug: () => {}, trace: () => {} },
140
- write: () => {},
141
- exec: async (command, attributes, options) => {
142
- await options.onPlusTag();
143
- await new Promise(resolve => (breakIdle = resolve));
144
- return { next: () => {} };
145
- }
146
- };
147
-
148
- let idlePromise = idleCommand(connection, 30000);
149
- await timers.drain();
150
-
151
- let armed = timers.pending();
152
- test.equal(armed.length, 1, 'the IDLE restart timer is armed');
153
- test.equal(armed[0].delay, 30000);
154
- test.ok(armed[0].unrefd, 'the background IDLE restart timer does not keep the process alive');
155
-
156
- breakIdle();
157
- await idlePromise;
158
- test.equal(timers.count(), 0, 'the restart timer is cleared when IDLE ends');
159
- test.done();
160
- });
161
- };
162
-
163
- module.exports['Timers: the throttle back-off timer is unrefd and cleared'] = async test => {
164
- await withFakeTimers(async timers => {
165
- let client = makeClient();
166
- client.socket = { destroyed: false, destroy: () => {} };
167
- client.writeSocket = client.socket;
168
-
169
- let request = { tag: 'A001', command: 'FETCH', resolve: () => {}, reject: () => {} };
170
- client.requestTagMap = new Map([['A001', request]]);
171
- client.currentRequest = { tag: 'A001', command: 'FETCH', sent: true };
172
-
173
- let served = false;
174
- client.streamer.read = () => {
175
- if (served) {
176
- return null;
177
- }
178
- served = true;
179
- return {
180
- payload: Buffer.from('A001 BAD Request is throttled. Suggested Backoff Time: 300000 milliseconds'),
181
- literals: [],
182
- next: () => {}
183
- };
184
- };
185
-
186
- let readerDone = client.reader().catch(() => {});
187
- await timers.drain();
188
-
189
- let backoff = timers.history().find(timer => timer.delay === 300000);
190
- test.ok(backoff, 'the throttle back-off timer is armed');
191
- test.ok(backoff.unrefd, 'the back-off timer does not keep the process alive');
192
-
193
- client.close();
194
- await readerDone;
195
-
196
- // history() returns snapshots, so re-read it after close()
197
- test.ok(timers.history().find(timer => timer.id === backoff.id).cleared, 'close() clears the back-off timer');
198
- test.equal(client._throttleWaits.size, 0);
199
- test.done();
200
- });
201
- };
202
-
203
- module.exports['Timers: throttleWait caps a server-suggested back-off'] = async test => {
204
- // The delay comes straight from a server hint (a Microsoft 365 "Suggested Backoff Time"),
205
- // which is unbounded. Asserted on the armed timer rather than by waiting it out.
206
- await withFakeTimers(async timers => {
207
- let client = makeClient({ port: 1 });
208
-
209
- let pending = client.throttleWait(7 * 24 * 3600 * 1000); // a week
210
- let armed = timers.history()[timers.history().length - 1];
211
- test.equal(armed.delay, 5 * 60 * 1000, 'the back-off is capped at five minutes');
212
- test.ok(armed.unrefd, 'the back-off timer must not keep the process alive');
213
-
214
- client.close();
215
- test.equal(await pending, true, 'close() aborts the wait');
216
- test.ok(timers.history().find(timer => timer.id === armed.id).cleared, 'close() clears the timer');
217
-
218
- // a delay under the cap is honored as given, and junk becomes an immediate wait
219
- let client2 = makeClient({ port: 1 });
220
- client2.throttleWait(1500);
221
- test.equal(timers.history()[timers.history().length - 1].delay, 1500);
222
- client2.throttleWait('nonsense');
223
- test.equal(timers.history()[timers.history().length - 1].delay, 0);
224
- client2.close();
225
- test.done();
226
- });
227
- };