imapflow 1.7.8 → 2.0.1

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