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,548 +0,0 @@
1
- 'use strict';
2
-
3
- // Tests for reliability/stability improvements: lock identity, acquireTimeout,
4
- // maxLockHoldTime diagnostic, capability-clear-on-STARTTLS, and related
5
- // cleanup paths.
6
-
7
- const { ImapFlow } = require('../lib/imap-flow');
8
- const iconv = require('iconv-lite');
9
-
10
- // Helper: client with stubbed socket/usable state so the fast path of
11
- // getMailboxLock() can grant locks synchronously (no network I/O).
12
- const makeClient = (overrides = {}) => {
13
- let client = new ImapFlow({
14
- host: 'imap.example.com',
15
- port: 993,
16
- auth: { user: 'test', pass: 'test' },
17
- logger: false,
18
- ...overrides
19
- });
20
- client.socket = { destroyed: false, destroy: () => {} };
21
- client.usable = true;
22
- client.mailbox = { path: 'INBOX', readOnly: false };
23
- return client;
24
- };
25
-
26
- // Helper: wait for any queued setImmediate callbacks (processLocks reschedules
27
- // itself via setImmediate after a release).
28
- const drain = () => new Promise(resolve => setImmediate(resolve));
29
-
30
- // ============================================================================
31
- // release() identity check
32
- // ============================================================================
33
-
34
- module.exports['Reliability: stale release() does not clear replacement lock'] = async test => {
35
- let client = makeClient();
36
-
37
- // Grant L1 via fast path
38
- let lockA = await client.getMailboxLock('INBOX');
39
- let staleRelease = lockA.release;
40
-
41
- // Release L1 normally
42
- staleRelease();
43
- await drain();
44
- test.equal(client.currentLock, false, 'L1 released');
45
-
46
- // Grant L2
47
- let lockB = await client.getMailboxLock('INBOX');
48
- test.ok(client.currentLock, 'L2 active');
49
- let currentBeforeStale = client.currentLock;
50
-
51
- // Stale call — must NOT clear L2's hold
52
- staleRelease();
53
- test.equal(client.currentLock, currentBeforeStale, 'L2 still active after stale release');
54
-
55
- lockB.release();
56
- await drain();
57
- test.done();
58
- };
59
-
60
- module.exports['Reliability: double release() is idempotent'] = async test => {
61
- let client = makeClient();
62
-
63
- let lock = await client.getMailboxLock('INBOX');
64
-
65
- // First release clears
66
- lock.release();
67
- await drain();
68
- test.equal(client.currentLock, false);
69
-
70
- // Second release is a no-op — must not throw, must not affect state
71
- test.doesNotThrow(() => lock.release());
72
- test.equal(client.currentLock, false);
73
-
74
- test.done();
75
- };
76
-
77
- // ============================================================================
78
- // acquireTimeout
79
- // ============================================================================
80
-
81
- module.exports['Reliability: acquireTimeout rejects with LockTimeout code'] = async test => {
82
- let client = makeClient();
83
-
84
- // Hold the first lock so the next one queues
85
- let lockA = await client.getMailboxLock('INBOX');
86
-
87
- let start = Date.now();
88
- try {
89
- await client.getMailboxLock('INBOX', { acquireTimeout: 30 });
90
- test.ok(false, 'Should have timed out');
91
- } catch (err) {
92
- let elapsed = Date.now() - start;
93
- test.equal(err.code, 'LockTimeout');
94
- test.ok(err.message.includes('Timed out'));
95
- test.ok(typeof err.lockId === 'number');
96
- test.ok(elapsed >= 25, `expected to wait ~30ms, got ${elapsed}`);
97
- }
98
-
99
- // Original lock must still be held
100
- test.ok(client.currentLock, 'L1 still held after L2 timeout');
101
-
102
- lockA.release();
103
- await drain();
104
- test.done();
105
- };
106
-
107
- module.exports['Reliability: acquireTimeout cleared when lock is granted'] = async test => {
108
- let client = makeClient();
109
-
110
- // Fast path grants immediately; timer never fires
111
- let lock = await client.getMailboxLock('INBOX', { acquireTimeout: 50 });
112
- test.ok(lock);
113
- test.ok(client.currentLock, 'L1 granted');
114
-
115
- // Wait past the timeout — no rejection should occur post-grant, no dangling timer
116
- await new Promise(r => setTimeout(r, 80));
117
- test.ok(client.currentLock, 'Still held after timer would have fired');
118
-
119
- lock.release();
120
- await drain();
121
- test.done();
122
- };
123
-
124
- module.exports['Reliability: acquireTimeout cleared on close()'] = async test => {
125
- let client = makeClient();
126
- // Hold L1 so L2 queues
127
- let lockA = await client.getMailboxLock('INBOX');
128
-
129
- let rejectedCode = null;
130
- client.getMailboxLock('INBOX', { acquireTimeout: 10_000 }).catch(err => {
131
- rejectedCode = err.code;
132
- });
133
-
134
- // Immediately close — pending lock should reject with NoConnection (not LockTimeout),
135
- // and its acquireTimer should be cleared so it never fires afterward.
136
- client.close();
137
-
138
- await drain();
139
- await new Promise(r => setTimeout(r, 30));
140
- test.equal(rejectedCode, 'NoConnection', 'Pending lock rejects with NoConnection on close');
141
-
142
- // Keep reference so linter doesn't complain
143
- test.ok(lockA);
144
- test.done();
145
- };
146
-
147
- // ============================================================================
148
- // maxLockHoldTime diagnostic
149
- // ============================================================================
150
-
151
- module.exports['Reliability: maxLockHoldTime warning fires when lock held past threshold'] = async test => {
152
- let warnings = [];
153
- let client = makeClient();
154
- client.log.warn = obj => warnings.push(obj);
155
-
156
- let lock = await client.getMailboxLock('INBOX', { maxLockHoldTime: 30 });
157
- await new Promise(r => setTimeout(r, 60));
158
-
159
- let hit = warnings.find(w => w && w.msg === 'Mailbox lock held for a long time');
160
- test.ok(hit, 'Warning log must fire');
161
- test.ok(typeof hit.heldFor === 'number' && hit.heldFor >= 25);
162
-
163
- lock.release();
164
- await drain();
165
- test.done();
166
- };
167
-
168
- module.exports['Reliability: maxLockHoldTime=0 disables the warning'] = async test => {
169
- let warnings = [];
170
- let client = makeClient();
171
- client.log.warn = obj => warnings.push(obj);
172
-
173
- let lock = await client.getMailboxLock('INBOX', { maxLockHoldTime: 0 });
174
- await new Promise(r => setTimeout(r, 30));
175
-
176
- test.ok(!warnings.some(w => w && w.msg === 'Mailbox lock held for a long time'), 'No warn when disabled');
177
-
178
- lock.release();
179
- await drain();
180
- test.done();
181
- };
182
-
183
- module.exports['Reliability: maxLockHoldTime=false disables the warning'] = async test => {
184
- let warnings = [];
185
- let client = makeClient();
186
- client.log.warn = obj => warnings.push(obj);
187
-
188
- let lock = await client.getMailboxLock('INBOX', { maxLockHoldTime: false });
189
- await new Promise(r => setTimeout(r, 30));
190
-
191
- test.ok(!warnings.some(w => w && w.msg === 'Mailbox lock held for a long time'));
192
-
193
- lock.release();
194
- await drain();
195
- test.done();
196
- };
197
-
198
- module.exports['Reliability: per-call maxLockHoldTime overrides constructor option'] = async test => {
199
- let warnings = [];
200
- // Constructor sets a long threshold; per-call sets a short one
201
- let client = makeClient({ maxLockHoldTime: 10_000 });
202
- client.log.warn = obj => warnings.push(obj);
203
-
204
- let lock = await client.getMailboxLock('INBOX', { maxLockHoldTime: 20 });
205
- await new Promise(r => setTimeout(r, 50));
206
-
207
- test.ok(
208
- warnings.some(w => w && w.msg === 'Mailbox lock held for a long time'),
209
- 'Per-call override must take effect'
210
- );
211
-
212
- lock.release();
213
- await drain();
214
- test.done();
215
- };
216
-
217
- module.exports['Reliability: held-lock timer cleared on release (does not fire after)'] = async test => {
218
- let warnings = [];
219
- let client = makeClient();
220
- client.log.warn = obj => warnings.push(obj);
221
-
222
- let lock = await client.getMailboxLock('INBOX', { maxLockHoldTime: 50 });
223
- lock.release();
224
- await drain();
225
-
226
- // Wait longer than the threshold — no warning should appear because release cleared it
227
- await new Promise(r => setTimeout(r, 80));
228
- test.ok(!warnings.some(w => w && w.msg === 'Mailbox lock held for a long time'));
229
-
230
- test.done();
231
- };
232
-
233
- module.exports['Reliability: held-lock timer cleared on close()'] = async test => {
234
- let warnings = [];
235
- let client = makeClient();
236
- client.log.warn = obj => warnings.push(obj);
237
-
238
- let lock = await client.getMailboxLock('INBOX', { maxLockHoldTime: 30 });
239
- test.ok(lock);
240
- client.close();
241
-
242
- await new Promise(r => setTimeout(r, 60));
243
- test.ok(!warnings.some(w => w && w.msg === 'Mailbox lock held for a long time'));
244
-
245
- test.done();
246
- };
247
-
248
- // ============================================================================
249
- // STARTTLS capability reset
250
- // ============================================================================
251
-
252
- module.exports['Reliability: STARTTLS code path clears capabilities before re-fetch'] = async test => {
253
- // Verify the clear() calls happen by driving the same branch directly.
254
- let client = new ImapFlow({
255
- host: 'imap.example.com',
256
- port: 993,
257
- auth: { user: 'test', pass: 'test' },
258
- logger: false
259
- });
260
-
261
- client.capabilities.set('LOGINDISABLED', true);
262
- client.capabilities.set('STARTTLS', true);
263
- client.authCapabilities.set('AUTH=PLAIN', false);
264
-
265
- // Stub the run() that would re-fetch CAPABILITY post-TLS
266
- let ran = [];
267
- client.run = async command => {
268
- ran.push(command);
269
- return true;
270
- };
271
-
272
- client.expectCapabilityUpdate = true;
273
- // Execute the same statements that the STARTTLS-success branch runs
274
- // (guards the behavior contract: caches are cleared before re-fetch)
275
- if (client.expectCapabilityUpdate) {
276
- client.capabilities.clear();
277
- client.authCapabilities.clear();
278
- await client.run('CAPABILITY');
279
- }
280
-
281
- test.equal(client.capabilities.size, 0, 'capabilities map cleared');
282
- test.equal(client.authCapabilities.size, 0, 'authCapabilities map cleared');
283
- test.deepEqual(ran, ['CAPABILITY']);
284
-
285
- test.done();
286
- };
287
-
288
- // ============================================================================
289
- // Handler try/catch (smoke tests via direct invocation)
290
- // ============================================================================
291
-
292
- module.exports['Reliability: sectionHandler throw is caught (no handler -> no effect)'] = async test => {
293
- let client = new ImapFlow({
294
- host: 'imap.example.com',
295
- port: 993,
296
- auth: { user: 'test', pass: 'test' },
297
- logger: false
298
- });
299
-
300
- // Install a section handler that throws
301
- client.getSectionHandler = () => async () => {
302
- throw new Error('handler boom');
303
- };
304
-
305
- // The production path awaits the handler inside a try/catch in reader().
306
- // Replicate that contract: the wrapping semantics here should not reject.
307
- let handler = client.getSectionHandler('TEST');
308
- let caught = false;
309
- try {
310
- // Mimic reader() — it does: try { await handler(...) } catch (err) { log.warn(...) }
311
- try {
312
- await handler([]);
313
- } catch (err) {
314
- caught = true;
315
- test.ok(err.message.includes('boom'));
316
- }
317
- } catch (unexpected) {
318
- test.ok(false, 'Outer catch should not observe: ' + unexpected.message);
319
- }
320
-
321
- test.ok(caught, 'Thrown error is captured by inner try/catch');
322
- test.done();
323
- };
324
-
325
- module.exports['Reliability: onPlusTag throw is caught (no handler -> no effect)'] = async test => {
326
- let client = new ImapFlow({
327
- host: 'imap.example.com',
328
- port: 993,
329
- auth: { user: 'test', pass: 'test' },
330
- logger: false
331
- });
332
- test.ok(client);
333
-
334
- // Simulate a currentRequest with a throwing onPlusTag
335
- let onPlusTag = async () => {
336
- throw new Error('plus tag boom');
337
- };
338
-
339
- // The reader() path wraps this call in try/catch; replicate the contract.
340
- let caught = false;
341
- try {
342
- await onPlusTag({});
343
- } catch (err) {
344
- caught = true;
345
- test.ok(err.message.includes('boom'));
346
- }
347
- test.ok(caught);
348
- test.done();
349
- };
350
-
351
- // ============================================================================
352
- // Charset decoder defensive listener
353
- // ============================================================================
354
-
355
- module.exports['Reliability: decoder emit(error) does not crash when user has not attached listener'] = test => {
356
- // Pattern: after a getDecoder() + defensive .on('error') + decoder.emit('error', err),
357
- // the process does not throw (because at least one listener was registered).
358
- let decoder = iconv.decodeStream('latin1');
359
-
360
- // Attach the same kind of safety listener the production code installs
361
- let warned = 0;
362
- decoder.on('error', () => {
363
- warned++;
364
- });
365
-
366
- // Simulate forwarding a source error into the decoder
367
- test.doesNotThrow(() => {
368
- decoder.emit('error', new Error('source stream failed'));
369
- });
370
- test.equal(warned, 1);
371
-
372
- test.done();
373
- };
374
-
375
- // ============================================================================
376
- // Throttle back-off timer is tracked and abortable on close()
377
- // ============================================================================
378
-
379
- // Feed a single throttling BAD response into reader() once, then null.
380
- const stubThrottleResponse = (client, backoffMs) => {
381
- let request = { tag: 'A001', command: 'FETCH', resolve: () => {}, reject: () => {} };
382
- client.requestTagMap = new Map([['A001', request]]);
383
- // The tagged response may only complete a command that was actually written to the socket, so
384
- // the stub has to present A001 as the active, already sent request - otherwise it is protocol
385
- // desynchronization.
386
- client.currentRequest = { tag: 'A001', command: 'FETCH', sent: true };
387
-
388
- let done = false;
389
- client.streamer.read = () => {
390
- if (done) {
391
- return null;
392
- }
393
- done = true;
394
- return {
395
- payload: Buffer.from(`A001 BAD Request is throttled. Suggested Backoff Time: ${backoffMs} milliseconds`),
396
- literals: [],
397
- next: () => {}
398
- };
399
- };
400
-
401
- return request;
402
- };
403
-
404
- module.exports['Reliability: throttle back-off aborts promptly on close()'] = async test => {
405
- let client = new ImapFlow({
406
- host: 'imap.example.com',
407
- port: 993,
408
- auth: { user: 'test', pass: 'test' },
409
- logger: false
410
- });
411
- client.socket = { destroyed: false, destroy: () => {} };
412
- client.writeSocket = client.socket;
413
-
414
- let rejected = null;
415
- let request = stubThrottleResponse(client, 300000); // 5 min back-off
416
- request.reject = err => {
417
- rejected = err;
418
- };
419
-
420
- let start = Date.now();
421
- let readerDone = client.reader().catch(() => {});
422
-
423
- // Let reader() reach the (tracked) back-off wait.
424
- await new Promise(r => setTimeout(r, 50));
425
- test.equal(client._throttleWaits.size, 1, 'back-off wait is tracked while waiting');
426
-
427
- client.close();
428
- await new Promise(r => setImmediate(r));
429
-
430
- test.ok(rejected, 'request rejected promptly after close()');
431
- test.equal(rejected.code, 'NoConnection', 'rejected with connection error, not ETHROTTLE');
432
- test.equal(client._throttleWaits.size, 0, 'throttle wait cleared on close()');
433
- test.ok(Date.now() - start < 5000, 'settled well under the 5-minute cap');
434
-
435
- await readerDone;
436
- test.done();
437
- };
438
-
439
- module.exports['Reliability: throttle back-off still rejects ETHROTTLE on normal expiry'] = async test => {
440
- let client = new ImapFlow({
441
- host: 'imap.example.com',
442
- port: 993,
443
- auth: { user: 'test', pass: 'test' },
444
- logger: false
445
- });
446
-
447
- let rejected = null;
448
- let request = stubThrottleResponse(client, 50); // 50ms back-off
449
- request.reject = err => {
450
- rejected = err;
451
- };
452
-
453
- let readerDone = client.reader().catch(() => {});
454
-
455
- await new Promise(r => setTimeout(r, 250));
456
-
457
- test.ok(rejected, 'request rejected after the back-off elapses');
458
- test.equal(rejected.code, 'ETHROTTLE', 'normal expiry still rejects ETHROTTLE');
459
- test.equal(rejected.throttleReset, 50, 'throttleReset preserved');
460
- test.equal(client._throttleWaits.size, 0, 'throttle wait cleared after normal expiry');
461
-
462
- await readerDone;
463
- client.close();
464
- test.done();
465
- };
466
-
467
- // ---------------------------------------------------------------------------
468
- // reader(): the parser backpressure callback is a resource that must always be released
469
- // ---------------------------------------------------------------------------
470
-
471
- module.exports['Reliability: an unexpected response-handling failure releases the parser and fails closed'] = async test => {
472
- // Several steps of response handling (log compilation, response shape assumptions, a handler
473
- // bug) sit outside the parse try block. A throw there used to propagate out of the reader loop
474
- // and leave ImapStream waiting on its backpressure callback forever - a silent permanent hang.
475
- let client = new ImapFlow({
476
- host: 'imap.example.com',
477
- port: 993,
478
- auth: { user: 'test', pass: 'test' },
479
- logger: false
480
- });
481
- client.socket = { destroyed: false, destroy: () => {} };
482
- client.writeSocket = client.socket;
483
-
484
- let rejected = null;
485
- let request = { tag: 'A001', command: 'NOOP', resolve: () => {}, reject: err => (rejected = err) };
486
- client.requestTagMap = new Map([['A001', request]]);
487
- client.currentRequest = { tag: 'A001', command: 'NOOP', sent: true };
488
-
489
- let errors = [];
490
- client.on('error', err => errors.push(err));
491
-
492
- let released = 0;
493
- let served = false;
494
- client.streamer.read = () => {
495
- if (served) {
496
- return null;
497
- }
498
- served = true;
499
- return {
500
- payload: Buffer.from('A001 OK NOOP done'),
501
- literals: [],
502
- next: () => released++
503
- };
504
- };
505
-
506
- // Stand in for any unexpected failure during response handling
507
- client.handleResponse = async () => {
508
- throw new Error('handler blew up');
509
- };
510
-
511
- await client.reader();
512
-
513
- test.equal(released, 1, 'the parser backpressure callback is released exactly once');
514
- test.ok(rejected, 'the in-flight request is rejected instead of hanging');
515
- test.equal(rejected.code, 'ResponseProcessingFailed');
516
- test.ok(client.streamer.destroyed, 'the parser stream is destroyed, so nothing further is parsed');
517
-
518
- await new Promise(resolve => setImmediate(resolve));
519
- test.ok(
520
- errors.some(err => err.code === 'ResponseProcessingFailed'),
521
- 'the failure is reported to the caller'
522
- );
523
-
524
- client.close();
525
- test.done();
526
- };
527
-
528
- module.exports['Reliability: releaseStreamData is idempotent'] = async test => {
529
- let client = new ImapFlow({
530
- host: 'imap.example.com',
531
- port: 993,
532
- auth: { user: 'test', pass: 'test' },
533
- logger: false
534
- });
535
-
536
- let released = 0;
537
- let data = { next: () => released++ };
538
-
539
- client.releaseStreamData(data);
540
- client.releaseStreamData(data);
541
- client.releaseStreamData(data);
542
-
543
- test.equal(released, 1, 'a readable item is only ever released once');
544
- test.doesNotThrow(() => client.releaseStreamData(null), 'releasing nothing is a no-op');
545
-
546
- client.close();
547
- test.done();
548
- };