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,593 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Tests for unhandled rejection prevention.
5
- *
6
- * When close() runs, it rejects pending promises synchronously. exec() and
7
- * getMailboxLock() attach .catch(noop) before returning, so the rejection is
8
- * observed immediately and does not trigger Node.js unhandledRejection.
9
- * These tests verify that no unhandled rejections escape while the caller
10
- * still receives the expected error.
11
- *
12
- * Key fix: exec() and getMailboxLock() are non-async, returning the promise
13
- * directly (with .catch(noop)), so the caller gets the same promise object
14
- * that has the noop handler. This prevents the async-wrapper double-promise
15
- * issue where .catch(noop) on an inner promise doesn't protect the outer
16
- * async wrapper promise.
17
- */
18
-
19
- const net = require('net');
20
- const { ImapFlow } = require('../lib/imap-flow');
21
-
22
- // Create a mock IMAP server with optional custom behavior.
23
- // options.extraCapabilities - additional capabilities (e.g., 'IDLE')
24
- // options.onCommand(socket, tag, command) - custom handler; return true if handled
25
- function createMockServer(options) {
26
- const extraCaps = options && options.extraCapabilities ? ' ' + options.extraCapabilities : '';
27
- const onCommand = options && options.onCommand;
28
-
29
- const server = net.createServer(socket => {
30
- socket.write('* OK Mock IMAP Server ready\r\n');
31
-
32
- socket.on('data', data => {
33
- const lines = data
34
- .toString()
35
- .split('\r\n')
36
- .filter(l => l.trim());
37
-
38
- for (const line of lines) {
39
- const parts = line.split(' ');
40
- const tag = parts[0];
41
- const command = parts[1] ? parts[1].toUpperCase() : '';
42
-
43
- if (onCommand && onCommand(socket, tag, command, line)) {
44
- continue;
45
- }
46
-
47
- if (command === 'CAPABILITY') {
48
- socket.write(`* CAPABILITY IMAP4rev1 AUTH=PLAIN${extraCaps}\r\n`);
49
- socket.write(`${tag} OK CAPABILITY completed\r\n`);
50
- } else if (command === 'LOGIN') {
51
- socket.write(`${tag} OK LOGIN completed\r\n`);
52
- } else if (command === 'LOGOUT') {
53
- socket.write('* BYE Server logging out\r\n');
54
- socket.write(`${tag} OK LOGOUT completed\r\n`);
55
- socket.end();
56
- } else if (command === 'NAMESPACE') {
57
- socket.write('* NAMESPACE (("" "/")) NIL NIL\r\n');
58
- socket.write(`${tag} OK NAMESPACE completed\r\n`);
59
- } else if (command === 'COMPRESS') {
60
- socket.write(`${tag} NO COMPRESS not supported\r\n`);
61
- } else if (command === 'ENABLE') {
62
- socket.write(`${tag} OK ENABLE completed\r\n`);
63
- } else if (command === 'ID') {
64
- socket.write('* ID NIL\r\n');
65
- socket.write(`${tag} OK ID completed\r\n`);
66
- } else if (command === 'SELECT' || command === 'EXAMINE') {
67
- socket.write('* 1 EXISTS\r\n');
68
- socket.write('* 1 RECENT\r\n');
69
- socket.write('* OK [UIDVALIDITY 1] UIDs valid\r\n');
70
- socket.write('* OK [UIDNEXT 2] Predicted next UID\r\n');
71
- socket.write(`${tag} OK SELECT completed\r\n`);
72
- } else if (command === 'NOOP') {
73
- socket.write(`${tag} OK NOOP completed\r\n`);
74
- } else if (tag && command) {
75
- socket.write(`${tag} OK Command completed\r\n`);
76
- }
77
- }
78
- });
79
-
80
- socket.on('error', () => {});
81
- });
82
-
83
- return server;
84
- }
85
-
86
- // Helper: install an unhandledRejection detector
87
- function installRejectionDetector(test) {
88
- let unhandled = false;
89
- let unhandledReason = null;
90
- const handler = reason => {
91
- unhandled = true;
92
- unhandledReason = reason;
93
- };
94
- process.on('unhandledRejection', handler);
95
-
96
- return {
97
- check() {
98
- process.removeListener('unhandledRejection', handler);
99
- test.equal(unhandled, false, 'no unhandledRejection should fire' + (unhandledReason ? ': ' + unhandledReason.message : ''));
100
- }
101
- };
102
- }
103
-
104
- // Drives a connection to the one state both IDLE-waiter tests need: IDLE issued but never
105
- // acknowledged with a "+", so preCheck() cannot send DONE and anything it queues stays queued
106
- // until close() tears the IDLE command down. Hands the connected client to `run`, and always
107
- // closes the server so the test ends.
108
- function withQueuedIdleWaiter(test, options, run, onError) {
109
- const server = createMockServer({
110
- extraCapabilities: 'IDLE',
111
- onCommand(socket, tag, command) {
112
- if (command === 'IDLE') {
113
- return true;
114
- }
115
- }
116
- });
117
-
118
- server.listen(0, '127.0.0.1', async () => {
119
- const client = new ImapFlow(
120
- Object.assign(
121
- {
122
- host: '127.0.0.1',
123
- port: server.address().port,
124
- secure: false,
125
- logger: false,
126
- disableAutoIdle: true,
127
- auth: { user: 'test', pass: 'test' }
128
- },
129
- options
130
- )
131
- );
132
-
133
- try {
134
- await client.connect();
135
- await client.mailboxOpen('INBOX');
136
-
137
- client.idle().catch(() => {
138
- // Expected: IDLE rejects when the connection is closed
139
- });
140
-
141
- // Let the IDLE command reach the server and install preCheck()
142
- await new Promise(r => setTimeout(r, 100));
143
- test.equal(typeof client.preCheck, 'function', 'IDLE should have installed preCheck()');
144
-
145
- await run(client);
146
- } catch (err) {
147
- if (onError) {
148
- onError(err);
149
- }
150
- test.ok(false, 'Unexpected error: ' + err.message);
151
- } finally {
152
- server.close(() => test.done());
153
- }
154
- });
155
- }
156
-
157
- exports['Unhandled Rejection Prevention'] = {
158
- 'exec() + close() race should not cause unhandled rejection'(test) {
159
- test.expect(2);
160
-
161
- const client = new ImapFlow({
162
- host: '127.0.0.1',
163
- port: 1,
164
- secure: false,
165
- logger: false
166
- });
167
-
168
- // Set up state so exec() doesn't reject synchronously
169
- client.state = client.states.AUTHENTICATED;
170
- client.socket = new net.Socket();
171
-
172
- const detector = installRejectionDetector(test);
173
-
174
- // Create a pending request, then immediately close.
175
- // close() rejects the request synchronously. The .catch(noop) on the
176
- // exec() promise ensures the rejection is observed immediately.
177
- let promise = client.exec('NOOP');
178
- client.close();
179
-
180
- // Wait for setImmediate and microtask rejections to settle
181
- setTimeout(() => {
182
- detector.check();
183
- promise.catch(err => {
184
- test.equal(err.code, 'NoConnection', 'caller should receive NoConnection error');
185
- test.done();
186
- });
187
- }, 100);
188
- },
189
-
190
- 'exec() on LOGOUT state should not cause unhandled rejection'(test) {
191
- test.expect(2);
192
-
193
- const client = new ImapFlow({
194
- host: '127.0.0.1',
195
- port: 1,
196
- secure: false,
197
- logger: false
198
- });
199
-
200
- client.state = client.states.LOGOUT;
201
-
202
- const detector = installRejectionDetector(test);
203
-
204
- // exec() detects LOGOUT and returns a rejected promise with .catch(noop)
205
- let promise = client.exec('NOOP');
206
-
207
- setTimeout(() => {
208
- detector.check();
209
- promise.catch(err => {
210
- test.equal(err.code, 'NoConnection', 'should reject with NoConnection');
211
- test.done();
212
- });
213
- }, 100);
214
- },
215
-
216
- 'exec() with destroyed socket should not cause unhandled rejection'(test) {
217
- test.expect(2);
218
-
219
- const client = new ImapFlow({
220
- host: '127.0.0.1',
221
- port: 1,
222
- secure: false,
223
- logger: false
224
- });
225
-
226
- client.state = client.states.AUTHENTICATED;
227
- let sock = new net.Socket();
228
- sock.destroy();
229
- client.socket = sock;
230
-
231
- const detector = installRejectionDetector(test);
232
-
233
- let promise = client.exec('NOOP');
234
-
235
- setTimeout(() => {
236
- detector.check();
237
- promise.catch(err => {
238
- test.equal(err.code, 'EConnectionClosed', 'should reject with EConnectionClosed');
239
- test.done();
240
- });
241
- }, 100);
242
- },
243
-
244
- 'multiple pending exec() + close() should not cause unhandled rejections'(test) {
245
- test.expect(7);
246
-
247
- const client = new ImapFlow({
248
- host: '127.0.0.1',
249
- port: 1,
250
- secure: false,
251
- logger: false
252
- });
253
-
254
- client.state = client.states.AUTHENTICATED;
255
- client.socket = new net.Socket();
256
-
257
- const detector = installRejectionDetector(test);
258
-
259
- let p1 = client.exec('NOOP');
260
- let p2 = client.exec('NOOP');
261
- let p3 = client.exec('NOOP');
262
- client.close();
263
-
264
- setTimeout(() => {
265
- detector.check();
266
-
267
- let settled = 0;
268
- const checkDone = () => {
269
- if (++settled === 3) {
270
- test.done();
271
- }
272
- };
273
-
274
- p1.catch(err => {
275
- test.ok(err, 'p1 should reject');
276
- test.equal(err.code, 'NoConnection', 'p1 error code');
277
- checkDone();
278
- });
279
- p2.catch(err => {
280
- test.ok(err, 'p2 should reject');
281
- test.equal(err.code, 'NoConnection', 'p2 error code');
282
- checkDone();
283
- });
284
- p3.catch(err => {
285
- test.ok(err, 'p3 should reject');
286
- test.equal(err.code, 'NoConnection', 'p3 error code');
287
- checkDone();
288
- });
289
- }, 100);
290
- },
291
-
292
- 'getMailboxLock() + close() race should not cause unhandled rejection'(test) {
293
- test.expect(2);
294
-
295
- const server = createMockServer();
296
-
297
- server.listen(0, '127.0.0.1', async () => {
298
- const port = server.address().port;
299
-
300
- const client = new ImapFlow({
301
- host: '127.0.0.1',
302
- port,
303
- secure: false,
304
- logger: false,
305
- auth: {
306
- user: 'test',
307
- pass: 'test'
308
- }
309
- });
310
-
311
- const detector = installRejectionDetector(test);
312
-
313
- try {
314
- await client.connect();
315
-
316
- // Request a lock, then immediately close before it resolves.
317
- // close() rejects pending locks synchronously.
318
- let lockPromise = client.getMailboxLock('INBOX');
319
- client.close();
320
-
321
- try {
322
- await lockPromise;
323
- test.ok(false, 'lockPromise should have rejected');
324
- } catch (err) {
325
- // Wait for any deferred unhandled rejection
326
- await new Promise(r => setTimeout(r, 100));
327
- detector.check();
328
- test.equal(err.code, 'NoConnection', 'caller should receive NoConnection error');
329
- }
330
- } catch (err) {
331
- detector.check();
332
- test.ok(false, 'unexpected error: ' + err.message);
333
- } finally {
334
- server.close(() => test.done());
335
- }
336
- });
337
- },
338
-
339
- 'connect() + close() during greeting timeout should not cause unhandled rejection'(test) {
340
- test.expect(2);
341
-
342
- // Server that accepts TCP but never sends a greeting.
343
- // The client's greetingTimeout will fire.
344
- const server = net.createServer(socket => {
345
- // Intentionally send nothing - let client timeout
346
- socket.on('error', () => {});
347
- });
348
-
349
- server.listen(0, '127.0.0.1', () => {
350
- const port = server.address().port;
351
-
352
- const client = new ImapFlow({
353
- host: '127.0.0.1',
354
- port,
355
- secure: false,
356
- logger: false,
357
- greetingTimeout: 200,
358
- auth: {
359
- user: 'test',
360
- pass: 'test'
361
- }
362
- });
363
-
364
- const detector = installRejectionDetector(test);
365
-
366
- // Close shortly after the TCP connection is established but
367
- // before the greeting is received
368
- const origSetSocket = client.setSocketHandlers;
369
- let closeTriggered = false;
370
- client.setSocketHandlers = function () {
371
- origSetSocket.call(this);
372
- if (!closeTriggered) {
373
- closeTriggered = true;
374
- // Close on the next tick to simulate race
375
- setImmediate(() => client.close());
376
- }
377
- };
378
-
379
- client.connect().then(
380
- () => {
381
- detector.check();
382
- test.ok(false, 'connect() should have rejected');
383
- server.close(() => test.done());
384
- },
385
- err => {
386
- setTimeout(() => {
387
- detector.check();
388
- test.ok(err && err.code, 'connect() should reject with an error code');
389
- server.close(() => test.done());
390
- }, 100);
391
- }
392
- );
393
- });
394
- },
395
-
396
- 'BYE during IDLE should not cause unhandled rejection (Death 1)'(test) {
397
- test.expect(2);
398
-
399
- const server = createMockServer({
400
- extraCapabilities: 'IDLE',
401
- onCommand(socket, tag, command) {
402
- if (command === 'IDLE') {
403
- socket.write('+ idling\r\n');
404
- // After a short delay, send BYE and close (simulates token expiry)
405
- setTimeout(() => {
406
- try {
407
- socket.write('* BYE Session invalidated - AccessTokenExpired\r\n');
408
- socket.end();
409
- } catch {
410
- // socket may already be closed
411
- }
412
- }, 50);
413
- return true;
414
- }
415
- }
416
- });
417
-
418
- server.listen(0, '127.0.0.1', async () => {
419
- const port = server.address().port;
420
-
421
- const client = new ImapFlow({
422
- host: '127.0.0.1',
423
- port,
424
- secure: false,
425
- logger: false,
426
- auth: {
427
- user: 'test',
428
- pass: 'test'
429
- }
430
- });
431
-
432
- const detector = installRejectionDetector(test);
433
-
434
- try {
435
- await client.connect();
436
- await client.mailboxOpen('INBOX');
437
-
438
- // Start IDLE and wait for the BYE-triggered close
439
- await new Promise((resolve, reject) => {
440
- client.idle().catch(() => {
441
- // Expected: IDLE rejects when BYE arrives
442
- });
443
-
444
- client.on('close', () => {
445
- // Wait for any deferred unhandled rejections to surface
446
- setTimeout(resolve, 100);
447
- });
448
-
449
- // Safety timeout
450
- setTimeout(() => reject(new Error('Timeout waiting for close')), 5000);
451
- });
452
-
453
- detector.check();
454
- test.ok(true, 'No unhandled rejection during IDLE + BYE');
455
- } catch (err) {
456
- detector.check();
457
- test.ok(false, 'Unexpected error: ' + err.message);
458
- } finally {
459
- server.close(() => test.done());
460
- }
461
- });
462
- },
463
-
464
- 'close() stamps the rejection site, command and connection id on the error'(test) {
465
- test.expect(5);
466
-
467
- const client = new ImapFlow({
468
- host: '127.0.0.1',
469
- port: 1,
470
- secure: false,
471
- logger: false,
472
- id: 'test-cid'
473
- });
474
-
475
- client.state = client.states.AUTHENTICATED;
476
- client.socket = new net.Socket();
477
-
478
- const detector = installRejectionDetector(test);
479
-
480
- let promise = client.exec('NOOP');
481
- client.close();
482
-
483
- setTimeout(() => {
484
- detector.check();
485
- promise.catch(err => {
486
- test.equal(err.code, 'NoConnection', 'caller should receive NoConnection error');
487
- test.equal(err.rejectedFrom, 'pendingRequest', 'error should name the rejection site');
488
- test.equal(err.command, 'NOOP', 'error should name the command it belonged to');
489
- test.equal(err.cid, 'test-cid', 'error should carry the connection id');
490
- test.done();
491
- });
492
- }, 100);
493
- },
494
-
495
- 'a preCheck() waiter rejection names its own site, not the IDLE command'(test) {
496
- test.expect(8);
497
-
498
- withQueuedIdleWaiter(test, { id: 'waiter-cid' }, async client => {
499
- let waiter = client.preCheck();
500
- client.close();
501
-
502
- try {
503
- await waiter;
504
- test.ok(false, 'the waiter should have rejected');
505
- } catch (err) {
506
- test.equal(err.rejectedFrom, 'preCheckWaiter', 'the waiter names its own rejection site');
507
- test.equal(err.cid, 'waiter-cid', 'the waiter error carries the connection id');
508
- test.equal(err.code, 'NoConnection', 'the original code is preserved for callers that branch on it');
509
- test.strictEqual(err.command, undefined, 'the command belongs to the site the error came from, not to the waiter');
510
- test.ok(err.cause, 'the command failure travels on as the cause');
511
- test.equal(err.cause.rejectedFrom, 'pendingRequest', 'the cause still names the command site');
512
- test.equal(err.cause.command, 'IDLE', 'the cause still names the command');
513
- }
514
- });
515
- },
516
-
517
- 'preCheck() waiter rejected by close() should not cause unhandled rejection'(test) {
518
- test.expect(2);
519
-
520
- const detector = installRejectionDetector(test);
521
-
522
- withQueuedIdleWaiter(
523
- test,
524
- {},
525
- async client => {
526
- // Request an IDLE break and drop the returned promise on the floor, so the queued
527
- // waiter has no handler of its own when close() rejects it
528
- client.preCheck();
529
- client.close();
530
-
531
- await new Promise(r => setTimeout(r, 100));
532
- detector.check();
533
- },
534
- () => detector.check()
535
- );
536
- },
537
-
538
- 'BAD response to FETCH should not cause unhandled rejection (Death 2)'(test) {
539
- test.expect(3);
540
-
541
- const server = createMockServer({
542
- onCommand(socket, tag, command) {
543
- if (command === 'UID' || command === 'FETCH') {
544
- // Simulate "Server Unavailable" error for any FETCH variant
545
- socket.write(`${tag} BAD Server Unavailable. 15\r\n`);
546
- return true;
547
- }
548
- }
549
- });
550
-
551
- server.listen(0, '127.0.0.1', async () => {
552
- const port = server.address().port;
553
-
554
- const client = new ImapFlow({
555
- host: '127.0.0.1',
556
- port,
557
- secure: false,
558
- logger: false,
559
- auth: {
560
- user: 'test',
561
- pass: 'test'
562
- }
563
- });
564
-
565
- const detector = installRejectionDetector(test);
566
-
567
- try {
568
- await client.connect();
569
- await client.mailboxOpen('INBOX');
570
-
571
- // Attempt a FETCH that will get BAD response
572
- try {
573
- await client.fetchOne('*', { uid: true }, { uid: true });
574
- test.ok(false, 'fetchOne should have rejected');
575
- } catch (err) {
576
- test.equal(err.message, 'Command failed', 'Should get Command failed error');
577
- test.equal(err.responseStatus, 'BAD', 'Response status should be BAD');
578
- }
579
-
580
- // Wait for any deferred unhandled rejections
581
- await new Promise(r => setTimeout(r, 100));
582
- detector.check();
583
-
584
- client.close();
585
- } catch (err) {
586
- detector.check();
587
- test.ok(false, 'Unexpected error: ' + err.message);
588
- } finally {
589
- server.close(() => test.done());
590
- }
591
- });
592
- }
593
- };