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,1828 +0,0 @@
1
- 'use strict';
2
-
3
- const { ImapFlow } = require('../lib/imap-flow');
4
- const { EventEmitter } = require('events');
5
- const net = require('net');
6
-
7
- // Edge Cases Tests
8
-
9
- module.exports['Connection Edge: Socket error during connection'] = test => {
10
- let client = new ImapFlow({
11
- host: 'imap.example.com',
12
- port: 993,
13
- auth: { user: 'test', pass: 'test' }
14
- });
15
-
16
- // Simulate socket error
17
- let errorEmitted = false;
18
- client.on('error', err => {
19
- errorEmitted = true;
20
- test.ok(err);
21
- test.equal(err._connId, client.id);
22
- });
23
-
24
- // Trigger error through emitError
25
- let testError = new Error('Socket connection failed');
26
- testError.code = 'ECONNREFUSED';
27
- client.emitError(testError);
28
-
29
- test.ok(errorEmitted, 'Error event should be emitted');
30
- test.done();
31
- };
32
-
33
- module.exports['Connection Edge: Write after socket destroyed'] = test => {
34
- let client = new ImapFlow({
35
- host: 'imap.example.com',
36
- port: 993,
37
- auth: { user: 'test', pass: 'test' }
38
- });
39
-
40
- // Simulate destroyed socket
41
- client.socket = { destroyed: true };
42
-
43
- test.throws(
44
- () => {
45
- client.write('TEST');
46
- },
47
- /Socket is already closed/,
48
- 'Should throw when writing to destroyed socket'
49
- );
50
-
51
- test.done();
52
- };
53
-
54
- module.exports['Connection Edge: Write after logout'] = test => {
55
- let client = new ImapFlow({
56
- host: 'imap.example.com',
57
- port: 993,
58
- auth: { user: 'test', pass: 'test' }
59
- });
60
-
61
- // Create a mock socket
62
- client.socket = { destroyed: false };
63
- client.state = client.states.LOGOUT;
64
-
65
- test.throws(
66
- () => {
67
- client.write('TEST');
68
- },
69
- /Can not send data after logged out/,
70
- 'Should throw when writing after logout'
71
- );
72
-
73
- test.done();
74
- };
75
-
76
- module.exports['Connection Edge: Stats reset functionality'] = test => {
77
- let client = new ImapFlow({
78
- host: 'imap.example.com',
79
- port: 993,
80
- auth: { user: 'test', pass: 'test' }
81
- });
82
-
83
- // Set some initial values
84
- client.writeBytesCounter = 100;
85
- client.streamer.readBytesCounter = 200;
86
-
87
- let stats = client.stats(false);
88
- test.equal(stats.sent, 100);
89
- test.equal(stats.received, 200);
90
-
91
- // Reset stats
92
- stats = client.stats(true);
93
- test.equal(stats.sent, 100, 'Should return old value before reset');
94
- test.equal(stats.received, 200, 'Should return old value before reset');
95
-
96
- // Check if reset worked
97
- test.equal(client.writeBytesCounter, 0, 'Write counter should be reset');
98
- test.equal(client.streamer.readBytesCounter, 0, 'Read counter should be reset');
99
-
100
- test.done();
101
- };
102
-
103
- module.exports['Connection Edge: Multiple error handlers'] = test => {
104
- let client = new ImapFlow({
105
- host: 'imap.example.com',
106
- port: 993,
107
- auth: { user: 'test', pass: 'test' }
108
- });
109
-
110
- let errorCount = 0;
111
-
112
- // Add multiple error handlers
113
- client.on('error', () => errorCount++);
114
- client.on('error', () => errorCount++);
115
- client.on('error', () => errorCount++);
116
-
117
- // Emit an error
118
- client.emitError(new Error('Test error'));
119
-
120
- test.equal(errorCount, 3, 'All error handlers should be called');
121
- test.done();
122
- };
123
-
124
- module.exports['Connection Edge: Connection with valid empty auth'] = test => {
125
- // ImapFlow actually allows creating client without throwing
126
- // The error would occur during connection
127
- let client = new ImapFlow({
128
- host: 'imap.example.com',
129
- port: 993,
130
- auth: { user: 'test' }
131
- });
132
-
133
- test.ok(client, 'Client should be created even with partial auth');
134
- test.equal(client.host, 'imap.example.com');
135
- test.done();
136
- };
137
-
138
- module.exports['Connection Edge: Connection with default values'] = test => {
139
- // ImapFlow sets default values for missing options
140
- let client = new ImapFlow({
141
- host: 'imap.example.com',
142
- auth: { user: 'test', pass: 'test' }
143
- });
144
-
145
- test.ok(client, 'Client should be created with defaults');
146
- test.equal(client.port, 143, 'Should use default port');
147
- test.equal(client.secureConnection, false, 'Should default to non-secure');
148
- test.done();
149
- };
150
-
151
- module.exports['Connection Edge: Port number handling'] = test => {
152
- // ImapFlow sets default port based on secure flag
153
- let client1 = new ImapFlow({
154
- host: 'imap.example.com',
155
- secure: false,
156
- auth: { user: 'test', pass: 'test' }
157
- });
158
- test.equal(client1.port, 143, 'Default non-secure port');
159
-
160
- let client2 = new ImapFlow({
161
- host: 'imap.example.com',
162
- port: 65535,
163
- auth: { user: 'test', pass: 'test' }
164
- });
165
- test.equal(client2.port, 65535, 'Custom port accepted');
166
-
167
- test.done();
168
- };
169
-
170
- module.exports['Connection Edge: STARTTLS misconfiguration'] = test => {
171
- let client = new ImapFlow({
172
- host: 'imap.example.com',
173
- port: 143,
174
- secure: true,
175
- doSTARTTLS: true,
176
- auth: { user: 'test', pass: 'test' }
177
- });
178
-
179
- // Test that upgradeToSTARTTLS throws on misconfiguration
180
- client.upgradeToSTARTTLS().catch(err => {
181
- test.ok(err);
182
- test.ok(err.message.includes('Misconfiguration'), 'Should detect STARTTLS misconfiguration');
183
- test.done();
184
- });
185
- };
186
-
187
- module.exports['Connection Edge: Socket timeout handling'] = test => {
188
- let client = new ImapFlow({
189
- host: 'imap.example.com',
190
- port: 993,
191
- auth: { user: 'test', pass: 'test' }
192
- });
193
-
194
- // Create a mock socket
195
- client.socket = new EventEmitter();
196
- client.socket.destroyed = false;
197
- client.socket.destroy = () => {};
198
- client.writeSocket = client.socket;
199
- client.writeSocket.destroy = () => {};
200
-
201
- let errorEmitted = false;
202
- client.on('error', err => {
203
- errorEmitted = true;
204
- test.equal(err.code, 'ETIMEOUT');
205
- });
206
-
207
- // Set up socket handlers
208
- client.setSocketHandlers();
209
-
210
- // Simulate timeout on non-idle connection
211
- client.idling = false;
212
- client.socket.emit('timeout');
213
-
214
- test.ok(errorEmitted, 'Timeout error should be emitted');
215
- test.done();
216
- };
217
-
218
- module.exports['Connection Edge: Socket timeout during IDLE'] = test => {
219
- let client = new ImapFlow({
220
- host: 'imap.example.com',
221
- port: 993,
222
- auth: { user: 'test', pass: 'test' }
223
- });
224
-
225
- // Create mock socket and methods
226
- client.socket = new EventEmitter();
227
- client.socket.destroyed = false;
228
- client.writeSocket = client.socket;
229
- client.usable = true;
230
- client.idling = true;
231
-
232
- // Mock run and idle methods
233
- let noopCalled = false;
234
- let idleCalled = false;
235
-
236
- client.run = async command => {
237
- if (command === 'NOOP') {
238
- noopCalled = true;
239
- return Promise.resolve();
240
- }
241
- };
242
-
243
- client.idle = async () => {
244
- idleCalled = true;
245
- return Promise.resolve();
246
- };
247
-
248
- // Set up socket handlers
249
- client.setSocketHandlers();
250
-
251
- // Simulate timeout during IDLE
252
- client.socket.emit('timeout');
253
-
254
- // Give async operations time to complete
255
- setTimeout(() => {
256
- test.ok(noopCalled, 'NOOP should be called to recover from IDLE timeout');
257
- // Returning to IDLE is autoidle()'s decision once the NOOP settles (run() re-arms it);
258
- // the watchdog handler itself must not bypass the busy guard by calling idle() directly
259
- test.equal(idleCalled, false, 'the timeout handler does not restart IDLE by itself');
260
- test.done();
261
- }, 100);
262
- };
263
-
264
- module.exports['Connection Edge: Clear socket handlers'] = test => {
265
- let client = new ImapFlow({
266
- host: 'imap.example.com',
267
- port: 993,
268
- auth: { user: 'test', pass: 'test' }
269
- });
270
-
271
- // Create a mock socket
272
- client.socket = new EventEmitter();
273
- client.writeSocket = client.socket;
274
-
275
- // Set handlers
276
- client.setSocketHandlers();
277
-
278
- // Verify handlers are set
279
- test.equal(client.socket.listenerCount('error'), 1);
280
- test.equal(client.socket.listenerCount('close'), 1);
281
- test.equal(client.socket.listenerCount('end'), 1);
282
- test.equal(client.socket.listenerCount('timeout'), 1);
283
-
284
- // Clear handlers
285
- client.clearSocketHandlers();
286
-
287
- // Verify handlers are removed
288
- test.equal(client.socket.listenerCount('error'), 0);
289
- test.equal(client.socket.listenerCount('close'), 0);
290
- test.equal(client.socket.listenerCount('end'), 0);
291
- test.equal(client.socket.listenerCount('timeout'), 0);
292
-
293
- test.done();
294
- };
295
-
296
- module.exports['Connection Edge: Write with null socket'] = test => {
297
- let client = new ImapFlow({
298
- host: 'imap.example.com',
299
- port: 993,
300
- auth: { user: 'test', pass: 'test' }
301
- });
302
-
303
- // Set socket to null
304
- client.socket = null;
305
-
306
- test.throws(
307
- () => {
308
- client.write('TEST');
309
- },
310
- /Socket is already closed/,
311
- 'Should throw when socket is null'
312
- );
313
-
314
- test.done();
315
- };
316
-
317
- module.exports['Connection Edge: Compression error handling'] = test => {
318
- let client = new ImapFlow({
319
- host: 'imap.example.com',
320
- port: 993,
321
- auth: { user: 'test', pass: 'test' }
322
- });
323
-
324
- // Create mock socket and streams
325
- client.socket = new EventEmitter();
326
- client.socket.pipe = () => client.socket;
327
- client.socket.unpipe = () => {};
328
- client.streamer = new EventEmitter();
329
-
330
- // Mock run method to simulate successful COMPRESS negotiation
331
- client.run = async command => {
332
- if (command === 'COMPRESS') {
333
- return true;
334
- }
335
- };
336
-
337
- let errorEmitted = false;
338
- client.streamer.on('error', err => {
339
- errorEmitted = true;
340
- test.ok(err);
341
- });
342
-
343
- // Call compress
344
- client.compress().then(() => {
345
- // Simulate compression error
346
- if (client._inflate) {
347
- client._inflate.emit('error', new Error('Compression failed'));
348
- }
349
-
350
- test.ok(errorEmitted, 'Compression error should be propagated');
351
- test.done();
352
- });
353
- };
354
-
355
- module.exports['Connection Edge: Authentication state after logout'] = test => {
356
- let client = new ImapFlow({
357
- host: 'imap.example.com',
358
- port: 993,
359
- auth: { user: 'test', pass: 'test' }
360
- });
361
-
362
- // Set initial authenticated state
363
- client.state = client.states.AUTHENTICATED;
364
- client.authenticated = true;
365
-
366
- // Simulate logout
367
- client.state = client.states.LOGOUT;
368
-
369
- // Try to authenticate
370
- client.authenticate().catch(err => {
371
- test.ok(err);
372
- test.equal(err.message, 'Already logged out');
373
- test.done();
374
- });
375
- };
376
-
377
- module.exports['Connection Edge: Throttling detection'] = test => {
378
- let client = new ImapFlow({
379
- host: 'imap.example.com',
380
- port: 993,
381
- auth: { user: 'test', pass: 'test' }
382
- });
383
-
384
- // Create mock request
385
- let errorReceived = null;
386
- let request = {
387
- tag: 'A001',
388
- command: 'FETCH',
389
- resolve: () => {},
390
- reject: err => {
391
- errorReceived = err;
392
- }
393
- };
394
-
395
- client.requestTagMap = new Map();
396
- client.requestTagMap.set('A001', request);
397
-
398
- // Simulate throttling response
399
- let parsed = {
400
- tag: 'A001',
401
- command: 'BAD',
402
- attributes: [
403
- {
404
- type: 'TEXT',
405
- value: 'Request is throttled. Suggested Backoff Time: 5000 milliseconds'
406
- }
407
- ]
408
- };
409
-
410
- // Mock streamer with proper async iterator
411
- client.streamer = new EventEmitter();
412
- client.streamer.read = () => null;
413
- client.streamer.iterate = async function* () {
414
- yield {
415
- next: () => {},
416
- parsed
417
- };
418
- };
419
-
420
- // Process reader
421
- client
422
- .reader()
423
- .then(() => {
424
- if (errorReceived) {
425
- test.ok(errorReceived, 'Should receive throttle error');
426
- test.equal(errorReceived.code, 'ETHROTTLE');
427
- test.equal(errorReceived.throttleReset, 5000);
428
- }
429
- test.done();
430
- })
431
- .catch(() => {
432
- // Reader might fail in test environment
433
- test.done();
434
- });
435
- };
436
-
437
- module.exports['Connection Edge: Binary data in write'] = test => {
438
- let client = new ImapFlow({
439
- host: 'imap.example.com',
440
- port: 993,
441
- auth: { user: 'test', pass: 'test' }
442
- });
443
-
444
- // Mock socket
445
- let writtenData = null;
446
- client.socket = { destroyed: false };
447
- client.writeSocket = {
448
- destroyed: false,
449
- write: data => {
450
- writtenData = data;
451
- }
452
- };
453
-
454
- // Test writing Buffer
455
- let testBuffer = Buffer.from('TEST');
456
- client.write(testBuffer);
457
-
458
- test.ok(Buffer.isBuffer(writtenData));
459
- test.ok(writtenData.includes(testBuffer));
460
- test.ok(writtenData.includes(Buffer.from('\r\n')));
461
-
462
- // Test writing string
463
- client.commandParts = [];
464
- client.write('STRING_TEST');
465
-
466
- test.ok(Buffer.isBuffer(writtenData));
467
- test.ok(writtenData.includes(Buffer.from('STRING_TEST\r\n')));
468
-
469
- test.done();
470
- };
471
-
472
- module.exports['Connection Edge: Invalid write data type'] = test => {
473
- let client = new ImapFlow({
474
- host: 'imap.example.com',
475
- port: 993,
476
- auth: { user: 'test', pass: 'test' }
477
- });
478
-
479
- // Mock socket
480
- client.socket = { destroyed: false };
481
- client.writeSocket = { destroyed: false, write: () => {} };
482
-
483
- // Test writing invalid data type
484
- let result = client.write(12345);
485
- test.equal(result, false, 'Should return false for invalid data type');
486
-
487
- result = client.write({ test: 'object' });
488
- test.equal(result, false, 'Should return false for object');
489
-
490
- test.done();
491
- };
492
-
493
- module.exports['Connection Edge: Concurrent connections'] = test => {
494
- let clients = [];
495
-
496
- // Create multiple clients
497
- for (let i = 0; i < 5; i++) {
498
- let client = new ImapFlow({
499
- host: `imap${i}.example.com`,
500
- port: 993,
501
- auth: { user: `user${i}`, pass: `pass${i}` }
502
- });
503
- clients.push(client);
504
- }
505
-
506
- // Verify each client has unique ID
507
- let ids = clients.map(c => c.id);
508
- let uniqueIds = [...new Set(ids)];
509
- test.equal(uniqueIds.length, clients.length, 'All client IDs should be unique');
510
-
511
- // Verify each client has independent state
512
- clients[0].state = clients[0].states.AUTHENTICATED;
513
- clients[1].state = clients[1].states.SELECTED;
514
-
515
- test.equal(clients[0].state, clients[0].states.AUTHENTICATED);
516
- test.equal(clients[1].state, clients[1].states.SELECTED);
517
- test.equal(clients[2].state, clients[2].states.NOT_AUTHENTICATED);
518
-
519
- test.done();
520
- };
521
-
522
- module.exports['Connection Edge: Destroyed write socket'] = test => {
523
- let client = new ImapFlow({
524
- host: 'imap.example.com',
525
- port: 993,
526
- auth: { user: 'test', pass: 'test' }
527
- });
528
-
529
- // Mock sockets
530
- client.socket = { destroyed: false };
531
- client.writeSocket = { destroyed: true };
532
-
533
- // Mock close method
534
- let closeCalled = false;
535
- client.close = () => {
536
- closeCalled = true;
537
- };
538
-
539
- // Attempt to write
540
- client.write('TEST');
541
-
542
- test.ok(closeCalled, 'Should call close when write socket is destroyed');
543
- test.done();
544
- };
545
-
546
- module.exports['Connection Edge: Command after logout'] = test => {
547
- let client = new ImapFlow({
548
- host: 'imap.example.com',
549
- port: 993,
550
- auth: { user: 'test', pass: 'test' }
551
- });
552
-
553
- // Set logout state
554
- client.state = client.states.LOGOUT;
555
-
556
- // Create a request
557
- let errorReceived = null;
558
- let request = {
559
- tag: 'A001',
560
- reject: err => {
561
- errorReceived = err;
562
- }
563
- };
564
-
565
- client.requestTagMap = new Map();
566
- client.requestTagMap.set('A001', request);
567
-
568
- // Try to send command
569
- client.send({ tag: 'A001', command: 'NOOP' }).then(() => {
570
- test.ok(errorReceived, 'Should reject command after logout');
571
- test.equal(errorReceived.code, 'NoConnection');
572
- test.done();
573
- });
574
- };
575
-
576
- module.exports['Connection Edge: Race condition in mailbox lock'] = test => {
577
- let client = new ImapFlow({
578
- host: 'imap.example.com',
579
- port: 993,
580
- auth: { user: 'test', pass: 'test' }
581
- });
582
-
583
- // Initialize locks array
584
- client.locks = [];
585
-
586
- // Simulate concurrent lock requests
587
- let lockPromises = [];
588
- for (let i = 0; i < 3; i++) {
589
- lockPromises.push(
590
- new Promise(resolve => {
591
- client.locks.push({
592
- path: 'INBOX',
593
- resolve,
594
- promise: new Promise(() => {})
595
- });
596
- })
597
- );
598
- }
599
-
600
- test.equal(client.locks.length, 3, 'Should queue multiple lock requests');
601
-
602
- // Process locks sequentially
603
- client.locks.forEach((lock, index) => {
604
- lock.resolve({ path: 'INBOX', index });
605
- });
606
-
607
- Promise.all(lockPromises).then(results => {
608
- test.equal(results.length, 3, 'All lock requests should be resolved');
609
- test.done();
610
- });
611
- };
612
-
613
- // NB! the capability discard/re-fetch behavior around STARTTLS is asserted end to
614
- // end in imap-flow-secure-test.js ('Secure: STARTTLS upgrade completes a session',
615
- // PRETLS-ONLY/POSTTLS-ONLY markers) - a mocked upgradeToSTARTTLS cannot reach the
616
- // re-fetch code without a real TLS handshake, so no vacuous variant is kept here.
617
-
618
- module.exports['Connection Edge: Event handlers attached before piping'] = test => {
619
- let client = new ImapFlow({
620
- host: 'imap.example.com',
621
- port: 993,
622
- auth: { user: 'test', pass: 'test' }
623
- });
624
-
625
- // Track the order of operations during the connection flow
626
- let pipeCalledBeforeHandlers = false;
627
- let eventHandlersAttached = false;
628
- let pipeWasCalled = false;
629
-
630
- // Override setEventHandlers to track when it's called
631
- let originalSetEventHandlers = client.setEventHandlers.bind(client);
632
- client.setEventHandlers = function () {
633
- eventHandlersAttached = true;
634
- return originalSetEventHandlers();
635
- };
636
-
637
- // Create a mock socket with pipe tracking
638
- let mockSocket = new EventEmitter();
639
- mockSocket.setKeepAlive = () => {};
640
- mockSocket.setTimeout = () => {};
641
- mockSocket.remotePort = 993;
642
- mockSocket.remoteAddress = '127.0.0.1';
643
- mockSocket.localAddress = '127.0.0.1';
644
- mockSocket.localPort = 12345;
645
- mockSocket.destroyed = false;
646
- mockSocket.pipe = function (dest) {
647
- pipeWasCalled = true;
648
- if (!eventHandlersAttached) {
649
- pipeCalledBeforeHandlers = true;
650
- }
651
- // Mock pipe behavior - just return the destination
652
- return dest;
653
- };
654
-
655
- // Assign mock socket
656
- client.socket = mockSocket;
657
- client.writeSocket = mockSocket;
658
-
659
- // Simulate the onConnect flow that happens in the actual code
660
- client.setSocketHandlers();
661
- client.setEventHandlers();
662
- client.socket.pipe(client.streamer);
663
-
664
- test.ok(eventHandlersAttached, 'Event handlers should be attached');
665
- test.ok(pipeWasCalled, 'Socket pipe should be called');
666
- test.ok(!pipeCalledBeforeHandlers, 'Event handlers should be attached before piping socket to streamer');
667
-
668
- test.done();
669
- };
670
-
671
- module.exports['Connection Edge: Pending locks rejected on close'] = test => {
672
- let client = new ImapFlow({
673
- host: 'imap.example.com',
674
- port: 993,
675
- auth: { user: 'test', pass: 'test' }
676
- });
677
-
678
- // Initialize locks array with pending locks
679
- let rejectedErrors = [];
680
- client.locks = [
681
- {
682
- path: 'INBOX',
683
- lockId: 'lock1',
684
- resolve: () => {},
685
- reject: err => rejectedErrors.push(err)
686
- },
687
- {
688
- path: 'Sent',
689
- lockId: 'lock2',
690
- resolve: () => {},
691
- reject: err => rejectedErrors.push(err)
692
- }
693
- ];
694
-
695
- // Mock socket to allow close() to run
696
- client.socket = { destroyed: false, destroy: () => {} };
697
- client.usable = true;
698
-
699
- // Call close
700
- client.close();
701
-
702
- // Rejections happen via setImmediate, so check after next tick
703
- setImmediate(() => {
704
- test.equal(rejectedErrors.length, 2, 'All pending locks should be rejected');
705
- test.equal(rejectedErrors[0].code, 'NoConnection', 'Error should have NoConnection code');
706
- test.equal(rejectedErrors[1].code, 'NoConnection', 'Error should have NoConnection code');
707
- test.equal(client.locks.length, 0, 'Locks array should be cleared');
708
- test.done();
709
- });
710
- };
711
-
712
- module.exports['Connection Edge: Lock rejection includes byeReason'] = test => {
713
- let client = new ImapFlow({
714
- host: 'imap.example.com',
715
- port: 993,
716
- auth: { user: 'test', pass: 'test' }
717
- });
718
-
719
- // Set byeReason before close
720
- client.byeReason = 'Server shutting down';
721
-
722
- let rejectedError = null;
723
- client.locks = [
724
- {
725
- path: 'INBOX',
726
- lockId: 'lock1',
727
- resolve: () => {},
728
- reject: err => {
729
- rejectedError = err;
730
- }
731
- }
732
- ];
733
-
734
- client.socket = { destroyed: false, destroy: () => {} };
735
- client.usable = true;
736
-
737
- client.close();
738
-
739
- setImmediate(() => {
740
- test.ok(rejectedError, 'Lock should be rejected');
741
- test.equal(rejectedError.code, 'NoConnection');
742
- test.equal(rejectedError.reason, 'Server shutting down', 'byeReason should be included');
743
- test.done();
744
- });
745
- };
746
-
747
- module.exports['Connection Edge: currentLock cleared on close'] = test => {
748
- let client = new ImapFlow({
749
- host: 'imap.example.com',
750
- port: 993,
751
- auth: { user: 'test', pass: 'test' }
752
- });
753
-
754
- // Simulate an active lock
755
- client.currentLock = {
756
- path: 'INBOX',
757
- lockId: 'active-lock',
758
- release: () => {}
759
- };
760
- client.locks = [];
761
-
762
- client.socket = { destroyed: false, destroy: () => {} };
763
- client.usable = true;
764
-
765
- client.close();
766
-
767
- test.equal(client.currentLock, false, 'currentLock should be cleared');
768
- test.done();
769
- };
770
-
771
- module.exports['Connection Edge: Lock rejection handles missing reject function'] = test => {
772
- let client = new ImapFlow({
773
- host: 'imap.example.com',
774
- port: 993,
775
- auth: { user: 'test', pass: 'test' }
776
- });
777
-
778
- // Create lock with missing reject function (edge case)
779
- let validRejected = false;
780
- client.locks = [
781
- {
782
- path: 'INBOX',
783
- lockId: 'lock1',
784
- resolve: () => {}
785
- // No reject function
786
- },
787
- {
788
- path: 'Sent',
789
- lockId: 'lock2',
790
- resolve: () => {},
791
- reject: () => {
792
- validRejected = true;
793
- }
794
- }
795
- ];
796
-
797
- client.socket = { destroyed: false, destroy: () => {} };
798
- client.usable = true;
799
-
800
- // Should not throw
801
- test.doesNotThrow(() => {
802
- client.close();
803
- }, 'Should handle missing reject function gracefully');
804
-
805
- setImmediate(() => {
806
- test.ok(validRejected, 'Valid lock should still be rejected');
807
- test.equal(client.locks.length, 0, 'Locks array should be cleared');
808
- test.done();
809
- });
810
- };
811
-
812
- module.exports['Connection Edge: Close with empty locks array'] = test => {
813
- let client = new ImapFlow({
814
- host: 'imap.example.com',
815
- port: 993,
816
- auth: { user: 'test', pass: 'test' }
817
- });
818
-
819
- client.locks = [];
820
- client.currentLock = false;
821
- client.socket = { destroyed: false, destroy: () => {} };
822
- client.usable = true;
823
-
824
- // Should not throw with empty locks
825
- test.doesNotThrow(() => {
826
- client.close();
827
- }, 'Should handle empty locks array');
828
-
829
- test.equal(client.currentLock, false);
830
- test.done();
831
- };
832
-
833
- module.exports['Connection Edge: Lock rejection happens synchronously during close'] = test => {
834
- let client = new ImapFlow({
835
- host: 'imap.example.com',
836
- port: 993,
837
- auth: { user: 'test', pass: 'test' }
838
- });
839
-
840
- let rejectionTime = null;
841
-
842
- client.locks = [
843
- {
844
- path: 'INBOX',
845
- lockId: 'lock1',
846
- resolve: () => {},
847
- reject: () => {
848
- rejectionTime = Date.now();
849
- }
850
- }
851
- ];
852
-
853
- client.socket = { destroyed: false, destroy: () => {} };
854
- client.usable = true;
855
-
856
- client.close();
857
-
858
- // Rejection should have happened synchronously during close()
859
- test.ok(rejectionTime !== null, 'Rejection should happen synchronously during close()');
860
- test.done();
861
- };
862
-
863
- module.exports['Connection Edge: Pending requests and locks both rejected on close'] = test => {
864
- let client = new ImapFlow({
865
- host: 'imap.example.com',
866
- port: 993,
867
- auth: { user: 'test', pass: 'test' }
868
- });
869
-
870
- let requestRejected = false;
871
- let lockRejected = false;
872
-
873
- // Add pending request - must be in requestQueue and requestTagMap
874
- let request = {
875
- tag: 'A001',
876
- reject: err => {
877
- requestRejected = true;
878
- test.equal(err.code, 'NoConnection');
879
- }
880
- };
881
- client.requestTagMap = new Map();
882
- client.requestTagMap.set('A001', request);
883
- client.requestQueue = [request];
884
-
885
- // Add pending lock
886
- client.locks = [
887
- {
888
- path: 'INBOX',
889
- lockId: 'lock1',
890
- resolve: () => {},
891
- reject: err => {
892
- lockRejected = true;
893
- test.equal(err.code, 'NoConnection');
894
- }
895
- }
896
- ];
897
-
898
- client.socket = { destroyed: false, destroy: () => {} };
899
- client.usable = true;
900
-
901
- client.close();
902
-
903
- // Rejections happen synchronously during close()
904
- test.ok(requestRejected, 'Pending request should be rejected');
905
- test.ok(lockRejected, 'Pending lock should be rejected');
906
- test.done();
907
- };
908
-
909
- module.exports['Connection Edge: exec throws NoConnection when in LOGOUT state'] = async test => {
910
- let client = new ImapFlow({
911
- host: 'imap.example.com',
912
- port: 993,
913
- auth: { user: 'test', pass: 'test' }
914
- });
915
-
916
- // Set state to LOGOUT
917
- client.state = client.states.LOGOUT;
918
- client.socket = { destroyed: false };
919
-
920
- try {
921
- await client.exec('NOOP', []);
922
- test.ok(false, 'Should have thrown');
923
- } catch (err) {
924
- test.equal(err.code, 'NoConnection');
925
- test.ok(err.message.includes('Connection not available'));
926
- }
927
-
928
- test.done();
929
- };
930
-
931
- module.exports['Connection Edge: exec throws NoConnection when isClosed is true'] = async test => {
932
- let client = new ImapFlow({
933
- host: 'imap.example.com',
934
- port: 993,
935
- auth: { user: 'test', pass: 'test' }
936
- });
937
-
938
- // Set isClosed flag
939
- client.isClosed = true;
940
- client.socket = { destroyed: false };
941
-
942
- try {
943
- await client.exec('NOOP', []);
944
- test.ok(false, 'Should have thrown');
945
- } catch (err) {
946
- test.equal(err.code, 'NoConnection');
947
- test.ok(err.message.includes('Connection not available'));
948
- }
949
-
950
- test.done();
951
- };
952
-
953
- module.exports['Connection Edge: getLogger emits log events when emitLogs is true'] = test => {
954
- let client = new ImapFlow({
955
- host: 'imap.example.com',
956
- port: 993,
957
- auth: { user: 'test', pass: 'test' },
958
- emitLogs: true
959
- });
960
-
961
- let logEvents = [];
962
- client.on('log', entry => {
963
- logEvents.push(entry);
964
- });
965
-
966
- // Trigger logging through the logger
967
- client.log.info({ msg: 'Test message', extra: 'data' });
968
-
969
- test.ok(logEvents.length > 0, 'Log event should be emitted');
970
- test.equal(logEvents[0].level, 'info');
971
- test.equal(logEvents[0].msg, 'Test message');
972
- test.equal(logEvents[0].extra, 'data');
973
- test.ok(logEvents[0].cid, 'Should have connection id');
974
- test.ok(logEvents[0].t, 'Should have timestamp');
975
- test.done();
976
- };
977
-
978
- module.exports['Connection Edge: getLogger emits log with error object'] = test => {
979
- let client = new ImapFlow({
980
- host: 'imap.example.com',
981
- port: 993,
982
- auth: { user: 'test', pass: 'test' },
983
- emitLogs: true
984
- });
985
-
986
- let logEvents = [];
987
- client.on('log', entry => {
988
- logEvents.push(entry);
989
- });
990
-
991
- // Trigger error logging
992
- let testError = new Error('Test error');
993
- testError.code = 'TEST_CODE';
994
- client.log.error({ msg: 'Error occurred', err: testError });
995
-
996
- test.ok(logEvents.length > 0, 'Log event should be emitted');
997
- test.equal(logEvents[0].level, 'error');
998
- test.ok(logEvents[0].err, 'Should have error object');
999
- test.ok(logEvents[0].err.stack, 'Error should have stack');
1000
- test.equal(logEvents[0].err.code, 'TEST_CODE', 'Error should have code');
1001
- test.done();
1002
- };
1003
-
1004
- module.exports['Connection Edge: unbind removes socket listeners and returns sockets'] = test => {
1005
- let client = new ImapFlow({
1006
- host: 'imap.example.com',
1007
- port: 993,
1008
- auth: { user: 'test', pass: 'test' }
1009
- });
1010
-
1011
- // Create mock socket with unpipe method
1012
- client.socket = new EventEmitter();
1013
- client.socket.unpipe = () => {};
1014
- client.writeSocket = client.socket;
1015
-
1016
- // Set up socket handlers first
1017
- client.setSocketHandlers();
1018
-
1019
- // Verify handlers are set
1020
- test.equal(client.socket.listenerCount('error'), 1);
1021
- test.equal(client.socket.listenerCount('close'), 1);
1022
-
1023
- // Call unbind
1024
- let result = client.unbind();
1025
-
1026
- // Verify handlers are removed
1027
- test.equal(client.socket.listenerCount('error'), 0);
1028
- test.equal(client.socket.listenerCount('close'), 0);
1029
- test.equal(client.socket.listenerCount('end'), 0);
1030
- test.equal(client.socket.listenerCount('timeout'), 0);
1031
-
1032
- // Verify return value
1033
- test.ok(result.readSocket, 'Should return readSocket');
1034
- test.ok(result.writeSocket, 'Should return writeSocket');
1035
- test.equal(result.readSocket, client.socket);
1036
- test.equal(result.writeSocket, client.socket);
1037
- // Non-compression path: the raw socket is the same object as read/write.
1038
- test.equal(result.socket, client.socket);
1039
-
1040
- test.done();
1041
- };
1042
-
1043
- module.exports['Connection Edge: unbind with inflate stream'] = test => {
1044
- let client = new ImapFlow({
1045
- host: 'imap.example.com',
1046
- port: 993,
1047
- auth: { user: 'test', pass: 'test' }
1048
- });
1049
-
1050
- // Create mock socket
1051
- client.socket = new EventEmitter();
1052
- client.socket.unpipe = () => {};
1053
-
1054
- // Create a separate writeSocket (also needs to be EventEmitter for setSocketHandlers)
1055
- client.writeSocket = new EventEmitter();
1056
- client.writeSocket.customProp = 'writeSocket';
1057
-
1058
- // Create mock inflate stream
1059
- client._inflate = new EventEmitter();
1060
- client._inflate.unpipe = () => {};
1061
-
1062
- // Set up socket handlers
1063
- client.setSocketHandlers();
1064
-
1065
- // Call unbind
1066
- let result = client.unbind();
1067
-
1068
- // Verify return value uses inflate for read and writeSocket for write
1069
- test.equal(result.readSocket, client._inflate);
1070
- test.equal(result.writeSocket.customProp, 'writeSocket');
1071
-
1072
- test.done();
1073
- };
1074
-
1075
- module.exports['Connection Edge: unbind survives post-handoff socket error (compression)'] = test => {
1076
- let client = new ImapFlow({
1077
- host: 'imap.example.com',
1078
- port: 993,
1079
- auth: { user: 'test', pass: 'test' }
1080
- });
1081
-
1082
- // Mock the compression topology: raw socket plus a separate PassThrough-like
1083
- // writeSocket, an inflate read stream and a deflate stream. The writeSocket
1084
- // and deflate error forwarders re-emit onto the raw socket, exactly like compress().
1085
- client.socket = new EventEmitter();
1086
- client.socket.unpipe = () => {};
1087
-
1088
- client.writeSocket = new EventEmitter();
1089
- client.writeSocket.on('error', err => {
1090
- if (client.socket) {
1091
- client.socket.emit('error', err);
1092
- }
1093
- });
1094
-
1095
- client._inflate = new EventEmitter();
1096
- client._inflate.unpipe = () => {};
1097
-
1098
- client._deflate = new EventEmitter();
1099
- client._deflate.on('error', err => {
1100
- if (client.socket) {
1101
- client.socket.emit('error', err);
1102
- }
1103
- });
1104
-
1105
- client.setSocketHandlers();
1106
-
1107
- let result = client.unbind();
1108
-
1109
- // Explicit contract: raw socket is now exposed alongside read/write sockets.
1110
- test.equal(result.socket, client.socket, 'Should expose the raw socket');
1111
- test.equal(result.readSocket, client._inflate);
1112
- test.equal(result.writeSocket, client.writeSocket);
1113
-
1114
- // ImapFlow's own _socketError must be detached from writeSocket; only the
1115
- // forwarder closure remains.
1116
- test.equal(client.writeSocket.listenerCount('error'), 1);
1117
-
1118
- // Emitting 'error' on a listener-less EventEmitter throws synchronously, so
1119
- // each of these would crash the host before the fix. After unbind() the
1120
- // benign listener on the orphaned raw socket must swallow them all.
1121
- test.doesNotThrow(() => {
1122
- client.socket.emit('error', Object.assign(new Error('upstream reset'), { code: 'ECONNRESET' }));
1123
- }, 'Direct socket error after unbind must not throw');
1124
-
1125
- test.doesNotThrow(() => {
1126
- client.writeSocket.emit('error', new Error('write failure'));
1127
- }, 'writeSocket error forwarded to the raw socket must not throw');
1128
-
1129
- test.doesNotThrow(() => {
1130
- client._deflate.emit('error', new Error('deflate failure'));
1131
- }, 'deflate error forwarded to the raw socket must not throw');
1132
-
1133
- // close() must remain safe to call after unbind().
1134
- test.doesNotThrow(() => {
1135
- client.close();
1136
- }, 'close() after unbind must not throw');
1137
-
1138
- test.done();
1139
- };
1140
-
1141
- module.exports['Connection Edge: resolveRange with number input'] = async test => {
1142
- let client = new ImapFlow({
1143
- host: 'imap.example.com',
1144
- port: 993,
1145
- auth: { user: 'test', pass: 'test' }
1146
- });
1147
-
1148
- client.mailbox = { exists: 10 };
1149
- let options = {};
1150
-
1151
- let result = await client.resolveRange(123, options);
1152
- test.equal(result, '123');
1153
- test.done();
1154
- };
1155
-
1156
- module.exports['Connection Edge: resolveRange with bigint input'] = async test => {
1157
- let client = new ImapFlow({
1158
- host: 'imap.example.com',
1159
- port: 993,
1160
- auth: { user: 'test', pass: 'test' }
1161
- });
1162
-
1163
- client.mailbox = { exists: 10 };
1164
- let options = {};
1165
-
1166
- let result = await client.resolveRange(BigInt(456), options);
1167
- test.equal(result, '456');
1168
- test.done();
1169
- };
1170
-
1171
- module.exports['Connection Edge: resolveRange with star and existing messages'] = async test => {
1172
- let client = new ImapFlow({
1173
- host: 'imap.example.com',
1174
- port: 993,
1175
- auth: { user: 'test', pass: 'test' }
1176
- });
1177
-
1178
- client.mailbox = { exists: 50 };
1179
- let options = { uid: true };
1180
-
1181
- let result = await client.resolveRange('*', options);
1182
- test.equal(result, '50');
1183
- test.equal(options.uid, false); // should be changed to sequence query
1184
- test.done();
1185
- };
1186
-
1187
- module.exports['Connection Edge: resolveRange with star and no messages'] = async test => {
1188
- let client = new ImapFlow({
1189
- host: 'imap.example.com',
1190
- port: 993,
1191
- auth: { user: 'test', pass: 'test' }
1192
- });
1193
-
1194
- client.mailbox = { exists: 0 };
1195
- let options = {};
1196
-
1197
- let result = await client.resolveRange('*', options);
1198
- test.equal(result, false);
1199
- test.done();
1200
- };
1201
-
1202
- module.exports['Connection Edge: resolveRange with {all: true}'] = async test => {
1203
- let client = new ImapFlow({
1204
- host: 'imap.example.com',
1205
- port: 993,
1206
- auth: { user: 'test', pass: 'test' }
1207
- });
1208
-
1209
- client.mailbox = { exists: 10 };
1210
- let options = {};
1211
-
1212
- let result = await client.resolveRange({ all: true }, options);
1213
- test.equal(result, '1:*');
1214
- test.done();
1215
- };
1216
-
1217
- module.exports['Connection Edge: resolveRange with {uid: value}'] = async test => {
1218
- let client = new ImapFlow({
1219
- host: 'imap.example.com',
1220
- port: 993,
1221
- auth: { user: 'test', pass: 'test' }
1222
- });
1223
-
1224
- client.mailbox = { exists: 10 };
1225
- let options = {};
1226
-
1227
- let result = await client.resolveRange({ uid: '1:100' }, options);
1228
- test.equal(result, '1:100');
1229
- test.equal(options.uid, true);
1230
- test.done();
1231
- };
1232
-
1233
- module.exports['Connection Edge: resolveRange with array input'] = async test => {
1234
- let client = new ImapFlow({
1235
- host: 'imap.example.com',
1236
- port: 993,
1237
- auth: { user: 'test', pass: 'test' }
1238
- });
1239
-
1240
- client.mailbox = { exists: 10 };
1241
- let options = {};
1242
-
1243
- let result = await client.resolveRange(['1', '2', '3'], options);
1244
- test.equal(result, '1,2,3');
1245
- test.done();
1246
- };
1247
-
1248
- module.exports['Connection Edge: resolveRange with empty/falsy input'] = async test => {
1249
- let client = new ImapFlow({
1250
- host: 'imap.example.com',
1251
- port: 993,
1252
- auth: { user: 'test', pass: 'test' }
1253
- });
1254
-
1255
- client.mailbox = { exists: 10 };
1256
- let options = {};
1257
-
1258
- let result = await client.resolveRange('', options);
1259
- test.equal(result, false);
1260
-
1261
- result = await client.resolveRange(null, options);
1262
- test.equal(result, false);
1263
-
1264
- test.done();
1265
- };
1266
-
1267
- module.exports['Connection Edge: ensureSelectedMailbox with no path'] = async test => {
1268
- let client = new ImapFlow({
1269
- host: 'imap.example.com',
1270
- port: 993,
1271
- auth: { user: 'test', pass: 'test' }
1272
- });
1273
-
1274
- let result = await client.ensureSelectedMailbox('');
1275
- test.equal(result, false);
1276
-
1277
- result = await client.ensureSelectedMailbox(null);
1278
- test.equal(result, false);
1279
-
1280
- test.done();
1281
- };
1282
-
1283
- module.exports['Connection Edge: ensureSelectedMailbox when already selected'] = async test => {
1284
- let client = new ImapFlow({
1285
- host: 'imap.example.com',
1286
- port: 993,
1287
- auth: { user: 'test', pass: 'test' }
1288
- });
1289
-
1290
- client.mailbox = { path: 'INBOX' };
1291
-
1292
- let result = await client.ensureSelectedMailbox('INBOX');
1293
- test.equal(result, true);
1294
- test.done();
1295
- };
1296
-
1297
- module.exports['Connection Edge: autoidle clears existing timer'] = test => {
1298
- let client = new ImapFlow({
1299
- host: 'imap.example.com',
1300
- port: 993,
1301
- auth: { user: 'test', pass: 'test' }
1302
- });
1303
-
1304
- // Set a fake timer
1305
- client.idleStartTimer = setTimeout(() => {}, 10000);
1306
- client.state = client.states.NOT_AUTHENTICATED;
1307
-
1308
- // Call autoidle - should clear the timer but not set new one (wrong state)
1309
- client.autoidle();
1310
-
1311
- // Should have been cleared
1312
- test.ok(true);
1313
- test.done();
1314
- };
1315
-
1316
- module.exports['Connection Edge: autoidle disabled by option'] = test => {
1317
- let client = new ImapFlow({
1318
- host: 'imap.example.com',
1319
- port: 993,
1320
- auth: { user: 'test', pass: 'test' },
1321
- disableAutoIdle: true
1322
- });
1323
-
1324
- client.state = client.states.SELECTED;
1325
-
1326
- // Call autoidle - should return early due to disableAutoIdle
1327
- client.autoidle();
1328
-
1329
- test.equal(client.idleStartTimer, undefined);
1330
- test.done();
1331
- };
1332
-
1333
- module.exports['Connection Edge: getUntaggedHandler with numeric command'] = test => {
1334
- let client = new ImapFlow({
1335
- host: 'imap.example.com',
1336
- port: 993,
1337
- auth: { user: 'test', pass: 'test' }
1338
- });
1339
-
1340
- // Set up a current request with untagged handler
1341
- client.currentRequest = {
1342
- options: {
1343
- untagged: {
1344
- FETCH: () => 'fetch handler'
1345
- }
1346
- }
1347
- };
1348
-
1349
- // Test with numeric command and FETCH attribute
1350
- let handler = client.getUntaggedHandler('123', [{ value: 'FETCH' }]);
1351
- test.equal(handler(), 'fetch handler');
1352
-
1353
- test.done();
1354
- };
1355
-
1356
- module.exports['Connection Edge: getUntaggedHandler from untaggedHandlers'] = test => {
1357
- let client = new ImapFlow({
1358
- host: 'imap.example.com',
1359
- port: 993,
1360
- auth: { user: 'test', pass: 'test' }
1361
- });
1362
-
1363
- // Set up global untagged handler
1364
- client.untaggedHandlers = {
1365
- CAPABILITY: () => 'capability handler'
1366
- };
1367
-
1368
- let handler = client.getUntaggedHandler('CAPABILITY', []);
1369
- test.equal(handler(), 'capability handler');
1370
-
1371
- test.done();
1372
- };
1373
-
1374
- module.exports['Connection Edge: getSectionHandler returns handler'] = test => {
1375
- let client = new ImapFlow({
1376
- host: 'imap.example.com',
1377
- port: 993,
1378
- auth: { user: 'test', pass: 'test' }
1379
- });
1380
-
1381
- // Set up section handler
1382
- client.sectionHandlers = {
1383
- CAPABILITY: () => 'section handler'
1384
- };
1385
-
1386
- let handler = client.getSectionHandler('CAPABILITY');
1387
- test.equal(handler(), 'section handler');
1388
-
1389
- // Test non-existent handler
1390
- handler = client.getSectionHandler('NONEXISTENT');
1391
- test.equal(handler, undefined);
1392
-
1393
- test.done();
1394
- };
1395
-
1396
- module.exports['Connection Edge: constructor with custom clientInfo'] = test => {
1397
- let client = new ImapFlow({
1398
- host: 'imap.example.com',
1399
- port: 993,
1400
- auth: { user: 'test', pass: 'test' },
1401
- clientInfo: {
1402
- name: 'CustomClient',
1403
- version: '1.0.0'
1404
- }
1405
- });
1406
-
1407
- test.equal(client.clientInfo.name, 'CustomClient');
1408
- test.equal(client.clientInfo.version, '1.0.0');
1409
- test.ok(client.clientInfo.vendor); // Should still have default vendor
1410
- test.done();
1411
- };
1412
-
1413
- module.exports['Connection Edge: constructor with secure port 993 detection'] = test => {
1414
- // When port is 993 and secure is undefined, should default to secure
1415
- let client = new ImapFlow({
1416
- host: 'imap.example.com',
1417
- port: 993,
1418
- auth: { user: 'test', pass: 'test' }
1419
- });
1420
-
1421
- test.equal(client.secureConnection, true);
1422
- test.done();
1423
- };
1424
-
1425
- module.exports['Connection Edge: constructor normalizes clientInfo diacritics'] = test => {
1426
- let client = new ImapFlow({
1427
- host: 'imap.example.com',
1428
- port: 993,
1429
- auth: { user: 'test', pass: 'test' },
1430
- clientInfo: {
1431
- name: 'Café Clïent',
1432
- version: '1.0.0'
1433
- }
1434
- });
1435
-
1436
- // Diacritics should be removed (NFD normalization splits é into e + accent, then accent is removed)
1437
- test.equal(client.clientInfo.name, 'Cafe Client');
1438
- test.done();
1439
- };
1440
-
1441
- module.exports['Connection Edge: constructor with servername option'] = test => {
1442
- let client = new ImapFlow({
1443
- host: '192.168.1.1',
1444
- port: 993,
1445
- auth: { user: 'test', pass: 'test' },
1446
- servername: 'mail.example.com'
1447
- });
1448
-
1449
- test.equal(client.servername, 'mail.example.com');
1450
- test.done();
1451
- };
1452
-
1453
- module.exports['Connection Edge: constructor servername from host when not IP'] = test => {
1454
- let client = new ImapFlow({
1455
- host: 'imap.example.com',
1456
- port: 993,
1457
- auth: { user: 'test', pass: 'test' }
1458
- });
1459
-
1460
- test.equal(client.servername, 'imap.example.com');
1461
- test.done();
1462
- };
1463
-
1464
- module.exports['Connection Edge: constructor servername false for IP host'] = test => {
1465
- let client = new ImapFlow({
1466
- host: '192.168.1.1',
1467
- port: 993,
1468
- auth: { user: 'test', pass: 'test' }
1469
- });
1470
-
1471
- test.equal(client.servername, false);
1472
- test.done();
1473
- };
1474
-
1475
- module.exports['Connection Edge: connect throws if called twice'] = async test => {
1476
- let client = new ImapFlow({
1477
- host: 'imap.example.com',
1478
- port: 993,
1479
- auth: { user: 'test', pass: 'test' }
1480
- });
1481
-
1482
- // Mark as already called
1483
- client._connectCalled = true;
1484
-
1485
- try {
1486
- await client.connect();
1487
- test.ok(false, 'Should have thrown');
1488
- } catch (err) {
1489
- test.ok(err.message.includes('re-use'));
1490
- }
1491
-
1492
- test.done();
1493
- };
1494
-
1495
- // Helper to create a mock client with compression enabled
1496
- async function setupCompressedClient() {
1497
- let client = new ImapFlow({
1498
- host: 'imap.example.com',
1499
- port: 993,
1500
- auth: { user: 'test', pass: 'test' }
1501
- });
1502
-
1503
- let mockSocket = new EventEmitter();
1504
- mockSocket.pipe = dest => dest;
1505
- mockSocket.unpipe = () => {};
1506
- mockSocket.destroy = () => {};
1507
- mockSocket.destroyed = false;
1508
- client.socket = mockSocket;
1509
- // Mock streamer that tracks destruction like a real stream, so close() drives it into
1510
- // the destroyed state the post-close guards actually check.
1511
- client.streamer = Object.assign(new EventEmitter(), {
1512
- destroyed: false,
1513
- destroy() {
1514
- this.destroyed = true;
1515
- }
1516
- });
1517
-
1518
- client.run = async command => {
1519
- if (command === 'COMPRESS') {
1520
- return true;
1521
- }
1522
- };
1523
-
1524
- await client.compress();
1525
- return { client, mockSocket };
1526
- }
1527
-
1528
- module.exports['Connection Edge: compress writeSocket error forwarded to socket when live'] = async test => {
1529
- let { client, mockSocket } = await setupCompressedClient();
1530
-
1531
- let errorReceived = false;
1532
- mockSocket.on('error', err => {
1533
- errorReceived = true;
1534
- test.equal(err.message, 'writeSocket error');
1535
- });
1536
-
1537
- client.writeSocket.emit('error', new Error('writeSocket error'));
1538
-
1539
- test.ok(errorReceived, 'Error should be forwarded to socket');
1540
- test.done();
1541
- };
1542
-
1543
- module.exports['Connection Edge: compress _deflate error forwarded to socket when live'] = async test => {
1544
- let { client, mockSocket } = await setupCompressedClient();
1545
-
1546
- let errorReceived = false;
1547
- mockSocket.on('error', err => {
1548
- errorReceived = true;
1549
- test.equal(err.message, 'deflate error');
1550
- });
1551
-
1552
- client._deflate.emit('error', new Error('deflate error'));
1553
-
1554
- test.ok(errorReceived, 'Error should be forwarded to socket');
1555
- test.done();
1556
- };
1557
-
1558
- module.exports['Connection Edge: compress readable after close does not crash'] = async test => {
1559
- let { client } = await setupCompressedClient();
1560
-
1561
- // Save reference before close() nulls it
1562
- let writeSocket = client.writeSocket;
1563
-
1564
- client.close();
1565
-
1566
- // Emit readable on the saved reference after close has nulled this.writeSocket
1567
- // This should not throw (guards at lines 1048 and 1021)
1568
- test.doesNotThrow(() => {
1569
- writeSocket.emit('readable');
1570
- });
1571
-
1572
- test.done();
1573
- };
1574
-
1575
- module.exports['Connection Edge: compress writeSocket error after close does not crash'] = async test => {
1576
- let { client } = await setupCompressedClient();
1577
-
1578
- let writeSocket = client.writeSocket;
1579
-
1580
- client.close();
1581
-
1582
- // Emit error on saved writeSocket after close has nulled this.socket
1583
- // This should not throw (guard at lines 1053-1055)
1584
- test.doesNotThrow(() => {
1585
- writeSocket.emit('error', new Error('late writeSocket error'));
1586
- });
1587
-
1588
- test.done();
1589
- };
1590
-
1591
- module.exports['Connection Edge: compress _deflate error after close does not crash'] = async test => {
1592
- let { client } = await setupCompressedClient();
1593
-
1594
- let deflate = client._deflate;
1595
-
1596
- client.close();
1597
-
1598
- // Emit error on saved _deflate after close has nulled this.socket
1599
- // This should not throw (guard at lines 1060-1062)
1600
- test.doesNotThrow(() => {
1601
- deflate.emit('error', new Error('late deflate error'));
1602
- });
1603
-
1604
- test.done();
1605
- };
1606
-
1607
- module.exports['Connection Edge: compress _inflate error after close does not crash'] = async test => {
1608
- let { client } = await setupCompressedClient();
1609
-
1610
- let inflate = client._inflate;
1611
-
1612
- // close() removes the streamer 'error' listener and destroys the streamer.
1613
- // A late inflate error must not be forwarded into the destroyed streamer
1614
- // (which would throw an unhandled 'error' and crash the process).
1615
- client.close();
1616
-
1617
- // Confirm we actually reached the post-close state the guard targets.
1618
- test.ok(client.streamer.destroyed, 'streamer is destroyed after close()');
1619
-
1620
- // Re-attach an 'error' listener so listenerCount('error') > 0: this isolates the
1621
- // `!streamer.destroyed` term of the guard. If forwarding still happened, this listener
1622
- // would throw and escape doesNotThrow — proving the destroyed-check is what suppresses it.
1623
- client.streamer.on('error', () => {
1624
- throw new Error('inflate error must not be forwarded into a destroyed streamer');
1625
- });
1626
-
1627
- test.doesNotThrow(() => {
1628
- inflate.emit('error', new Error('late inflate error'));
1629
- });
1630
-
1631
- test.done();
1632
- };
1633
-
1634
- // ============================================
1635
- // Mailbox Lock Tests
1636
- // ============================================
1637
-
1638
- module.exports['Connection Edge: getMailboxLock queues lock in locks array'] = async test => {
1639
- let client = new ImapFlow({
1640
- host: 'imap.example.com',
1641
- port: 993,
1642
- auth: { user: 'test', pass: 'test' }
1643
- });
1644
-
1645
- // getMailboxLock should push to locks array then reject (no connection)
1646
- try {
1647
- await client.getMailboxLock('INBOX');
1648
- test.ok(false, 'Should have thrown');
1649
- } catch (err) {
1650
- test.ok(err);
1651
- test.equal(err.code, 'NoConnection');
1652
- }
1653
-
1654
- test.done();
1655
- };
1656
-
1657
- module.exports['Connection Edge: lockCounter increments with each getMailboxLock call'] = async test => {
1658
- let client = new ImapFlow({
1659
- host: 'imap.example.com',
1660
- port: 993,
1661
- auth: { user: 'test', pass: 'test' }
1662
- });
1663
-
1664
- let initial = client.lockCounter;
1665
- let p1 = client.getMailboxLock('INBOX').catch(() => {});
1666
- let p2 = client.getMailboxLock('Sent').catch(() => {});
1667
-
1668
- test.equal(client.lockCounter, initial + 2);
1669
-
1670
- await Promise.all([p1, p2]);
1671
- test.done();
1672
- };
1673
-
1674
- module.exports['Connection Edge: lock rejected when connection not usable'] = async test => {
1675
- let client = new ImapFlow({
1676
- host: 'imap.example.com',
1677
- port: 993,
1678
- auth: { user: 'test', pass: 'test' }
1679
- });
1680
-
1681
- // usable is false by default, no socket
1682
- try {
1683
- await client.getMailboxLock('INBOX');
1684
- test.ok(false, 'Should have thrown');
1685
- } catch (err) {
1686
- test.ok(err);
1687
- test.equal(err.code, 'NoConnection');
1688
- test.ok(err.message.includes('not available'));
1689
- }
1690
- test.done();
1691
- };
1692
-
1693
- module.exports['Connection Edge: processLocks with empty locks array completes'] = async test => {
1694
- let client = new ImapFlow({
1695
- host: 'imap.example.com',
1696
- port: 993,
1697
- auth: { user: 'test', pass: 'test' }
1698
- });
1699
-
1700
- // processLocks with no pending locks should complete without error
1701
- await client.processLocks();
1702
- test.equal(client.processingLock, false);
1703
- test.done();
1704
- };
1705
-
1706
- module.exports['Connection Edge: multiple locks rejected when no connection'] = async test => {
1707
- let client = new ImapFlow({
1708
- host: 'imap.example.com',
1709
- port: 993,
1710
- auth: { user: 'test', pass: 'test' }
1711
- });
1712
-
1713
- // Queue multiple locks, all should reject
1714
- let errors = [];
1715
- let p1 = client.getMailboxLock('INBOX').catch(err => errors.push(err));
1716
- let p2 = client.getMailboxLock('Sent').catch(err => errors.push(err));
1717
- let p3 = client.getMailboxLock('Drafts').catch(err => errors.push(err));
1718
-
1719
- await Promise.all([p1, p2, p3]);
1720
-
1721
- test.equal(errors.length, 3);
1722
- for (let err of errors) {
1723
- test.equal(err.code, 'NoConnection');
1724
- }
1725
- test.done();
1726
- };
1727
-
1728
- // ============================================
1729
- // reader() survives a write() throw without stalling the stream
1730
- // ============================================
1731
-
1732
- module.exports['Connection Edge: reader survives write throw on + continuation'] = async test => {
1733
- let client = new ImapFlow({
1734
- host: 'imap.example.com',
1735
- port: 993,
1736
- auth: { user: 'test', pass: 'test' },
1737
- logger: false
1738
- });
1739
-
1740
- client.currentRequest = false;
1741
- client.commandParts = [Buffer.from('literal-bytes')];
1742
-
1743
- let writeAttempted = false;
1744
- client.write = () => {
1745
- writeAttempted = true;
1746
- throw new Error('write boom');
1747
- };
1748
-
1749
- let nextCalled = false;
1750
- let done = false;
1751
- client.streamer.read = () => {
1752
- if (done) {
1753
- return null;
1754
- }
1755
- done = true;
1756
- return {
1757
- payload: Buffer.from('+ Ready'),
1758
- literals: [],
1759
- next: () => {
1760
- nextCalled = true;
1761
- }
1762
- };
1763
- };
1764
-
1765
- let rejected = false;
1766
- await client.reader().catch(() => {
1767
- rejected = true;
1768
- });
1769
-
1770
- test.ok(writeAttempted, 'the literal-continuation write path was exercised');
1771
- test.equal(rejected, false, 'reader did not reject on write throw');
1772
- test.ok(nextCalled, 'data.next() was still called so the stream is not stalled');
1773
- test.done();
1774
- };
1775
-
1776
- // ============================================
1777
- // BYE greeting surfaces its reason on the connect rejection
1778
- // ============================================
1779
-
1780
- module.exports['Connection Edge: BYE greeting surfaces reason on connect rejection'] = async test => {
1781
- // Deterministic ordering: the server ends the socket only AFTER the client has parsed the
1782
- // BYE and set byeReason, instead of racing a fixed 50ms timer that could flake on slow CI.
1783
- let signalByeParsed;
1784
- let byeParsed = new Promise(resolve => {
1785
- signalByeParsed = resolve;
1786
- });
1787
-
1788
- let server = net.createServer(socket => {
1789
- socket.on('error', () => {});
1790
- socket.write('* BYE Server too busy\r\n');
1791
- byeParsed.then(() => socket.end());
1792
- });
1793
-
1794
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
1795
- let port = server.address().port;
1796
-
1797
- let client = new ImapFlow({
1798
- host: '127.0.0.1',
1799
- port,
1800
- secure: false,
1801
- disableAutoIdle: true,
1802
- logger: false,
1803
- auth: { user: 'test', pass: 'test' }
1804
- });
1805
- client.on('error', () => {});
1806
-
1807
- // Signal once the client's BYE handler has run, so byeReason is guaranteed set before close().
1808
- let origServerBye = client.serverBye.bind(client);
1809
- client.serverBye = async parsed => {
1810
- await origServerBye(parsed);
1811
- signalByeParsed();
1812
- };
1813
-
1814
- let connectErr = null;
1815
- try {
1816
- await client.connect();
1817
- test.ok(false, 'connect() should reject after a BYE greeting');
1818
- } catch (err) {
1819
- connectErr = err;
1820
- }
1821
-
1822
- test.ok(connectErr, 'connect() rejected');
1823
- test.equal(connectErr.reason, 'Server too busy', 'BYE reason surfaced on the rejection');
1824
-
1825
- client.close();
1826
- server.close();
1827
- test.done();
1828
- };