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,725 +0,0 @@
1
- 'use strict';
2
-
3
- // Targeted unit tests for low-level ImapFlow internals that are awkward to reach
4
- // through a full session: emitError routing, write() guards, run() dispatch
5
- // guards, the synthetic logger, the streamer error handler, autoidle scheduling
6
- // and the untaggedFetch flag/modseq branches.
7
-
8
- const { ImapFlow } = require('../lib/imap-flow');
9
- const { withFakeTimers } = require('./fixtures/fake-timers');
10
- const { makeClient, makeIdleReadyClient } = require('./fixtures/test-client');
11
-
12
- // ============================================================================
13
- // emitError
14
- // ============================================================================
15
-
16
- module.exports['Internals: emitError ignores falsy error'] = test => {
17
- let client = makeClient();
18
- let emitted = false;
19
- client.on('error', () => {
20
- emitted = true;
21
- });
22
- client.emitError(null);
23
- test.equal(emitted, false);
24
- test.done();
25
- };
26
-
27
- module.exports['Internals: emitError routes to upgrade rejector while upgrading'] = test => {
28
- let client = makeClient();
29
- client.socket = { destroyed: true, destroy: () => {} };
30
- client.upgrading = true;
31
- let rejected = null;
32
- // Stands in for the upgrade's settle() helper, which owns clearing `upgrading`,
33
- // the upgrade timer and the temporary handshake handlers.
34
- client._upgradeReject = err => {
35
- rejected = err;
36
- client.upgrading = false;
37
- };
38
- let err = new Error('tls boom');
39
- client.emitError(err);
40
- test.equal(rejected, err);
41
- test.equal(client._upgradeReject, null, 'the rejector is consumed exactly once');
42
- test.equal(client.upgrading, false);
43
- test.done();
44
- };
45
-
46
- module.exports['Internals: emitError closes when an upgrade has no rejector'] = test => {
47
- let client = makeClient();
48
- client.socket = { destroyed: true, destroy: () => {} };
49
- client.upgrading = true;
50
- client._upgradeReject = null;
51
-
52
- let emitted = false;
53
- client.on('error', () => {
54
- emitted = true;
55
- });
56
-
57
- client.emitError(new Error('tls boom'));
58
-
59
- test.equal(client.upgrading, false, 'the upgrade flag is cleared');
60
- test.equal(emitted, false, 'no duplicate error event while the upgrade owns reporting');
61
- client.close();
62
- test.done();
63
- };
64
-
65
- module.exports['Internals: emitError rejects pending connect promise'] = test => {
66
- let client = makeClient();
67
- client.socket = { destroyed: true, destroy: () => {} };
68
- let rejected = null;
69
- client.initialReject = err => {
70
- rejected = err;
71
- };
72
- client.initialResolve = () => {};
73
- let err = new Error('connect boom');
74
- client.emitError(err);
75
- test.equal(rejected, err);
76
- test.equal(client.initialReject, false);
77
- test.done();
78
- };
79
-
80
- module.exports['Internals: emitError emits error event as fallback'] = test => {
81
- let client = makeClient();
82
- client.socket = { destroyed: true, destroy: () => {} };
83
- let emitted = null;
84
- client.on('error', err => {
85
- emitted = err;
86
- });
87
- let err = new Error('plain boom');
88
- client.emitError(err);
89
- test.equal(emitted, err);
90
- test.equal(err._connId, client.id);
91
- test.done();
92
- };
93
-
94
- // ============================================================================
95
- // streamer error handler
96
- // ============================================================================
97
-
98
- module.exports['Internals: streamer error handler silently closes on transient codes'] = test => {
99
- let client = makeClient();
100
- let closeAfterCalled = false;
101
- client.closeAfter = () => {
102
- closeAfterCalled = true;
103
- };
104
- let emitted = false;
105
- client.on('error', () => {
106
- emitted = true;
107
- });
108
- client._streamerErrorHandler({ code: 'ECONNRESET', message: 'reset' });
109
- test.ok(closeAfterCalled);
110
- test.equal(emitted, false);
111
- test.done();
112
- };
113
-
114
- module.exports['Internals: streamer error handler emits on other codes'] = test => {
115
- let client = makeClient();
116
- client.socket = { destroyed: true, destroy: () => {} };
117
- let emitted = null;
118
- client.on('error', err => {
119
- emitted = err;
120
- });
121
- client._streamerErrorHandler({ code: 'EOTHER', message: 'weird' });
122
- test.ok(emitted);
123
- test.done();
124
- };
125
-
126
- // ============================================================================
127
- // write()
128
- // ============================================================================
129
-
130
- module.exports['Internals: write throws when socket destroyed'] = test => {
131
- let client = makeClient();
132
- client.socket = { destroyed: true };
133
- test.throws(() => client.write('A NOOP'), /Socket is already closed/);
134
- test.done();
135
- };
136
-
137
- module.exports['Internals: write throws after logout'] = test => {
138
- let client = makeClient();
139
- client.socket = { destroyed: false };
140
- client.state = client.states.LOGOUT;
141
- test.throws(() => client.write('A NOOP'), /Can not send data after logged out/);
142
- test.done();
143
- };
144
-
145
- module.exports['Internals: write closes when writeSocket destroyed'] = test => {
146
- let client = makeClient();
147
- client.socket = { destroyed: false };
148
- client.writeSocket = { destroyed: true };
149
- client.state = client.states.AUTHENTICATED;
150
- let closed = false;
151
- client.close = () => {
152
- closed = true;
153
- };
154
- let res = client.write('A NOOP');
155
- test.equal(res, undefined);
156
- test.ok(closed);
157
- test.done();
158
- };
159
-
160
- module.exports['Internals: write returns false for non-string non-buffer'] = test => {
161
- let client = makeClient();
162
- let written = [];
163
- client.socket = { destroyed: false };
164
- client.writeSocket = { destroyed: false, write: c => written.push(c) };
165
- client.state = client.states.AUTHENTICATED;
166
- client.commandParts = [];
167
- let res = client.write({ not: 'a buffer' });
168
- test.equal(res, false);
169
- test.equal(written.length, 0);
170
- test.done();
171
- };
172
-
173
- // A writable client whose raw traffic log is captured, for the two logRaw branches
174
- const makeRawLogClient = rawSensitiveCommand => {
175
- let logs = [];
176
- let written = [];
177
- let client = makeClient({ logRaw: true });
178
- client.log = { trace: o => logs.push(o), debug: () => {}, warn: () => {}, error: () => {}, info: () => {} };
179
- client.socket = { destroyed: false };
180
- client.writeSocket = { destroyed: false, write: c => written.push(c) };
181
- client.rawSensitiveCommand = rawSensitiveCommand;
182
- return { client, logs, written };
183
- };
184
-
185
- module.exports['Internals: write logs raw data when logRaw enabled'] = test => {
186
- let { client, logs, written } = makeRawLogClient(false);
187
- client.write('A NOOP');
188
- let entry = logs.find(l => l.src === 'c' && l.msg === 'write to socket');
189
- test.ok(entry);
190
- test.equal(Buffer.from(entry.data, 'base64').toString(), 'A NOOP\r\n');
191
- test.ok(!entry.hidden);
192
- test.equal(written.length, 1);
193
- test.done();
194
- };
195
-
196
- module.exports['Internals: write withholds raw data for a credential-bearing command'] = test => {
197
- // send() sets this for LOGIN/AUTHENTICATE before the first frame reaches the socket
198
- let { client, logs, written } = makeRawLogClient(true);
199
- client.write('A1 LOGIN "user" "hunter2"');
200
- let entry = logs.find(l => l.src === 'c' && l.msg === 'write to socket');
201
- test.ok(entry);
202
- test.ok(entry.hidden);
203
- // The placeholder is fixed width, so the entry cannot disclose the password length
204
- test.equal(Buffer.from(entry.data, 'base64').toString(), '(* value hidden *)\r\n');
205
- // The frame itself is still written to the socket unchanged
206
- test.equal(written[0].toString(), 'A1 LOGIN "user" "hunter2"\r\n');
207
- test.done();
208
- };
209
-
210
- module.exports['Internals: send marks credential-bearing commands for the raw log'] = async test => {
211
- let client = makeClient();
212
- let written = [];
213
- client.socket = { destroyed: false };
214
- client.writeSocket = { destroyed: false, write: c => written.push(c) };
215
-
216
- // Lower case on purpose: the wire protocol is case-insensitive and exec() passes the
217
- // caller's spelling through unchanged, so the classification must normalize it
218
- await client.send({
219
- tag: 'A1',
220
- command: 'login',
221
- attributes: [
222
- { type: 'STRING', value: 'user' },
223
- { type: 'STRING', value: 'hunter2', sensitive: true }
224
- ],
225
- options: {}
226
- });
227
- test.equal(client.rawSensitiveCommand, true);
228
-
229
- await client.send({ tag: 'A2', command: 'NOOP', attributes: [], options: {} });
230
- test.equal(client.rawSensitiveCommand, false);
231
-
232
- // A command outside the list still masks if it marks an attribute sensitive, so the
233
- // declarative marker alone is enough to keep a new command out of the raw log. Nested
234
- // because the command compiler honors the marker at any depth.
235
- await client.send({
236
- tag: 'A3',
237
- command: 'SETMETADATA',
238
- attributes: [{ type: 'ATOM', value: 'INBOX' }, [{ type: 'STRING', value: 'token', sensitive: true }]],
239
- options: {}
240
- });
241
- test.equal(client.rawSensitiveCommand, true);
242
-
243
- test.done();
244
- };
245
-
246
- module.exports['Internals: write appends CRLF only on final part'] = test => {
247
- let client = makeClient();
248
- let written = [];
249
- client.socket = { destroyed: false };
250
- client.writeSocket = { destroyed: false, write: c => written.push(c) };
251
- client.state = client.states.AUTHENTICATED;
252
- // Pending command parts => no CRLF appended to this chunk
253
- client.commandParts = ['more'];
254
- client.write(Buffer.from('literal'));
255
- test.equal(written[0].toString(), 'literal');
256
- test.done();
257
- };
258
-
259
- // ============================================================================
260
- // run()
261
- // ============================================================================
262
-
263
- module.exports['Internals: run returns false for unknown command'] = async test => {
264
- let client = makeClient();
265
- client.socket = { destroyed: false };
266
- let res = await client.run('NOT_A_COMMAND');
267
- test.equal(res, false);
268
- test.done();
269
- };
270
-
271
- module.exports['Internals: run throws NoConnection without socket'] = async test => {
272
- let client = makeClient();
273
- client.socket = null;
274
- let err = null;
275
- try {
276
- await client.run('NOOP');
277
- } catch (e) {
278
- err = e;
279
- }
280
- test.ok(err);
281
- test.equal(err.code, 'NoConnection');
282
- test.done();
283
- };
284
-
285
- module.exports['Internals: run invokes preCheck and command handler'] = async test => {
286
- let client = makeClient();
287
- client.socket = { destroyed: false };
288
- let preCheckCalled = false;
289
- client.preCheck = async () => {
290
- preCheckCalled = true;
291
- };
292
- let handlerArgs = null;
293
- client.commands = new Map([
294
- [
295
- 'TESTCMD',
296
- async (conn, a, b) => {
297
- handlerArgs = [a, b];
298
- return 'handled';
299
- }
300
- ]
301
- ]);
302
- let res = await client.run('TESTCMD', 1, 2);
303
- test.equal(res, 'handled');
304
- test.ok(preCheckCalled);
305
- test.deepEqual(handlerArgs, [1, 2]);
306
- test.done();
307
- };
308
-
309
- // ============================================================================
310
- // autoidle
311
- // ============================================================================
312
-
313
- module.exports['Internals: autoidle does nothing when not selected'] = test => {
314
- let client = makeClient();
315
- client.state = client.states.AUTHENTICATED;
316
- client.autoidle();
317
- test.equal(client.idleStartTimer, undefined);
318
- test.done();
319
- };
320
-
321
- module.exports['Internals: autoidle schedules idle when selected'] = async test => {
322
- await withFakeTimers(async timers => {
323
- let client = makeIdleReadyClient();
324
-
325
- let idleCalled = false;
326
- client.idle = async () => {
327
- idleCalled = true;
328
- };
329
-
330
- client.autoidle();
331
- await timers.fire();
332
-
333
- test.ok(idleCalled);
334
- });
335
- test.done();
336
- };
337
-
338
- // ============================================================================
339
- // getLogger
340
- // ============================================================================
341
-
342
- module.exports['Internals: getLogger uses provided logger object'] = test => {
343
- let entries = [];
344
- let custom = {
345
- trace: o => entries.push(['trace', o]),
346
- debug: o => entries.push(['debug', o]),
347
- info: o => entries.push(['info', o]),
348
- warn: o => entries.push(['warn', o]),
349
- error: o => entries.push(['error', o]),
350
- fatal: o => entries.push(['fatal', o])
351
- };
352
- let client = makeClient({ logger: custom });
353
- client.log.info({ msg: 'hello' });
354
- test.ok(entries.some(e => e[0] === 'info'));
355
- test.done();
356
- };
357
-
358
- module.exports['Internals: getLogger falls back to console for missing fatal/error level'] = test => {
359
- // Logger object missing the 'error' method -> falls through to console.error
360
- let partial = {
361
- trace() {},
362
- debug() {},
363
- info() {},
364
- warn() {}
365
- // no error, no fatal
366
- };
367
- let client = makeClient({ logger: partial });
368
- let origConsoleError = console.error;
369
- let logged = [];
370
- console.error = (...args) => logged.push(args);
371
- try {
372
- let err = new Error('boom failure');
373
- err.code = 'XBOOM';
374
- // The answer is often one level down: this library attaches the underlying failure
375
- // as an enumerable `_err`
376
- err._err = Object.assign(new Error('inner failure'), { code: 'ECONNREFUSED' });
377
- client.log.error({ msg: 'boom', err });
378
- // A circular structure must not throw out of the log call, and must not be dropped
379
- let circular = { msg: 'loop' };
380
- circular.self = circular;
381
- client.log.error(circular);
382
- } finally {
383
- console.error = origConsoleError;
384
- }
385
- test.equal(logged.length, 2);
386
- // The Error was flattened, so message, stack and enumerable fields survive stringify
387
- let entry = JSON.parse(logged[0][0]);
388
- test.equal(entry.msg, 'boom');
389
- test.equal(entry.err.message, 'boom failure');
390
- test.equal(entry.err.code, 'XBOOM');
391
- test.ok(entry.err.stack);
392
- test.equal(entry.err._err.message, 'inner failure');
393
- test.equal(entry.err._err.code, 'ECONNREFUSED');
394
- // Unserializable entries still reach console.error, just not as JSON
395
- test.equal(logged[1][0].msg, 'loop');
396
- test.done();
397
- };
398
-
399
- module.exports['Internals: getLogger keeps cause and AggregateError members'] = test => {
400
- let client = makeClient({ emitLogs: true });
401
- let entries = [];
402
- client.on('log', entry => entries.push(entry));
403
-
404
- let inner = Object.assign(new Error('inner failure'), { code: 'ECONNREFUSED' });
405
- client.log.error({ msg: 'wrapped', err: new Error('outer failure', { cause: inner }) });
406
- // Node reports a multi-address connect failure as an AggregateError
407
- client.log.error({ msg: 'aggregate', err: new AggregateError([inner], 'all attempts failed') });
408
-
409
- test.equal(entries[0].err.cause.message, 'inner failure');
410
- test.equal(entries[0].err.cause.code, 'ECONNREFUSED');
411
- test.equal(entries[1].err.errors.length, 1);
412
- test.equal(entries[1].err.errors[0].message, 'inner failure');
413
- test.done();
414
- };
415
-
416
- module.exports['Internals: getLogger bounds a looping and a deep error chain'] = test => {
417
- let client = makeClient({ emitLogs: true });
418
- let entries = [];
419
- client.on('log', entry => entries.push(entry));
420
-
421
- // A chain that loops back must terminate rather than recurse forever
422
- let looping = new Error('looping failure');
423
- looping._err = looping;
424
- client.log.error({ msg: 'loop', err: looping });
425
- test.equal(entries[0].err.message, 'looping failure');
426
- test.equal(entries[0].err._err, 'looping failure');
427
-
428
- // A chain longer than the depth cap is truncated rather than walked to the end
429
- let deep = new Error('level 0');
430
- for (let i = 1; i <= 6; i++) {
431
- deep = Object.assign(new Error(`level ${i}`), { _err: deep });
432
- }
433
- client.log.error({ msg: 'deep', err: deep });
434
- test.equal(entries[1].err._err._err._err.message, 'level 3');
435
- // Past the cap the chain collapses to messages instead of being walked to the end
436
- test.equal(entries[1].err._err._err._err._err, 'level 2');
437
-
438
- test.done();
439
- };
440
-
441
- module.exports['Internals: getLogger never throws out of a log call'] = test => {
442
- let client = makeClient({ emitLogs: true });
443
- let entries = [];
444
- client.on('log', entry => entries.push(entry));
445
-
446
- // A throwing property getter on the logged error must not escape
447
- let hostile = {
448
- get message() {
449
- throw new Error('getter blew up');
450
- },
451
- stack: 'x'
452
- };
453
- test.doesNotThrow(() => client.log.warn({ msg: 'hostile', err: hostile }));
454
-
455
- // Neither must a throwing 'log' listener
456
- client.on('log', () => {
457
- throw new Error('listener blew up');
458
- });
459
- test.doesNotThrow(() => client.log.warn({ msg: 'still fine' }));
460
-
461
- test.done();
462
- };
463
-
464
- module.exports['Internals: getLogger emits log events when emitLogs set'] = test => {
465
- let client = makeClient({ logger: false });
466
- client.emitLogs = true;
467
- let events = [];
468
- client.on('log', entry => events.push(entry));
469
- let err = new Error('with stack');
470
- err.code = 'XCODE';
471
- client.log.warn({ msg: 'warned', err });
472
- test.equal(events.length, 1);
473
- test.equal(events[0].level, 'warn');
474
- test.equal(events[0].err.code, 'XCODE');
475
- test.ok(events[0].err.stack);
476
- test.done();
477
- };
478
-
479
- module.exports['Internals: logger:false suppresses output but still allows log calls'] = test => {
480
- let client = makeClient({ logger: false });
481
- // Should not throw
482
- client.log.debug({ msg: 'nothing happens' });
483
- test.ok(true);
484
- test.done();
485
- };
486
-
487
- // ============================================================================
488
- // Misc branch coverage
489
- // ============================================================================
490
-
491
- module.exports['Internals: secure connection defaults to port 993'] = test => {
492
- let client = new ImapFlow({ secure: true });
493
- test.equal(client.port, 993);
494
- test.done();
495
- };
496
-
497
- module.exports['Internals: getUntaggedHandler ignores non-string type token'] = test => {
498
- let client = makeClient();
499
- client.untaggedHandlers = { 5: () => 'numeric-keyword-handler' };
500
- // numeric prefix but attributes[0].value is not a string -> keyword stays '5'
501
- let handler = client.getUntaggedHandler('5', [{ type: 'ATOM', value: 12345 }]);
502
- test.equal(handler(), 'numeric-keyword-handler');
503
- test.done();
504
- };
505
-
506
- module.exports['Internals: untaggedVanished filters non-string tag entries'] = async test => {
507
- let client = makeClient();
508
- client.mailbox = { path: 'INBOX' };
509
- let events = [];
510
- client.on('expunge', e => events.push(e));
511
- await client.untaggedVanished({
512
- attributes: [[{ value: 12345 }], { value: '7' }] // non-string tag value filtered out
513
- });
514
- test.equal(events.length, 1);
515
- test.equal(events[0].earlier, false);
516
- test.done();
517
- };
518
-
519
- module.exports['Internals: setFlagColor returns false when STORE yields falsy'] = async test => {
520
- let client = makeClient();
521
- client.mailbox = { path: 'INBOX', exists: 5 };
522
- client.socket = { destroyed: false };
523
- client.run = async () => false; // both add and remove STORE return falsy
524
- let res = await client.setFlagColor('1', 'red');
525
- test.equal(res, false);
526
- test.done();
527
- };
528
-
529
- module.exports['Internals: fetchOne with falsy seq coerces to empty string'] = async test => {
530
- let client = makeClient();
531
- client.mailbox = { path: 'INBOX', exists: 5 };
532
- let captured = null;
533
- client.run = async (cmd, seq) => {
534
- captured = seq;
535
- return { list: [] };
536
- };
537
- let res = await client.fetchOne(0, {});
538
- test.equal(captured, ''); // (0 || '').toString()
539
- test.equal(res, false);
540
- test.done();
541
- };
542
-
543
- module.exports['Internals: unbind falls back to socket when writeSocket missing'] = test => {
544
- let client = makeClient();
545
- let raw = {
546
- unpipe() {},
547
- on() {},
548
- once() {},
549
- removeListener() {}
550
- };
551
- client.socket = raw;
552
- client.writeSocket = null;
553
- client.streamer = {/* unused here */};
554
- let result = client.unbind();
555
- test.equal(result.writeSocket, raw);
556
- test.done();
557
- };
558
-
559
- // ============================================================================
560
- // untaggedFetch flag/modseq branches
561
- // ============================================================================
562
-
563
- module.exports['Internals: untaggedFetch includes modseq and flagColor'] = async test => {
564
- let client = makeClient();
565
- client.mailbox = { path: 'INBOX', exists: 5 };
566
- let evt = null;
567
- client.on('flags', e => {
568
- evt = e;
569
- });
570
- let untagged = {
571
- command: '2',
572
- attributes: [
573
- { type: 'ATOM', value: 'FETCH' },
574
- [
575
- { type: 'ATOM', value: 'UID' },
576
- { type: 'ATOM', value: '20' },
577
- { type: 'ATOM', value: 'MODSEQ' },
578
- [{ type: 'ATOM', value: '4242' }],
579
- { type: 'ATOM', value: 'FLAGS' },
580
- [{ type: 'ATOM', value: '\\Flagged' }]
581
- ]
582
- ]
583
- };
584
- await client.untaggedFetch(untagged);
585
- test.ok(evt);
586
- test.equal(evt.uid, 20);
587
- test.ok(evt.modseq);
588
- // \\Flagged with no MailFlagBit keywords maps to the red color
589
- test.equal(evt.flagColor, 'red');
590
- test.done();
591
- };
592
-
593
- // ============================================================================
594
- // runInternal dispatch guards
595
- // ============================================================================
596
-
597
- module.exports['Internals: runInternal returns false for unknown command'] = async test => {
598
- let client = makeClient();
599
- client.socket = { destroyed: false };
600
- let res = await client.runInternal('NOT_A_COMMAND');
601
- test.equal(res, false);
602
- test.done();
603
- };
604
-
605
- module.exports['Internals: runInternal throws NoConnection when the socket is destroyed'] = async test => {
606
- let client = makeClient();
607
- client.socket = { destroyed: true };
608
- let err = null;
609
- try {
610
- await client.runInternal('NOOP');
611
- } catch (e) {
612
- err = e;
613
- }
614
- test.ok(err);
615
- test.equal(err.code, 'NoConnection');
616
- test.done();
617
- };
618
-
619
- // ============================================================================
620
- // send() onSend error containment
621
- // ============================================================================
622
-
623
- module.exports['Internals: send contains a throwing onSend callback'] = async test => {
624
- let client = makeClient();
625
- let written = [];
626
- let warnings = [];
627
- client.socket = { destroyed: false };
628
- client.writeSocket = { destroyed: false, write: chunk => written.push(chunk) };
629
- client.state = client.states.AUTHENTICATED;
630
- client.log.warn = entry => warnings.push(entry);
631
- client.currentRequest = { tag: 'A1', sent: false };
632
-
633
- // The command is already on the wire when onSend runs, so a throwing callback
634
- // must be logged and swallowed instead of rejecting the request
635
- await client.send({
636
- tag: 'A1',
637
- command: 'NOOP',
638
- attributes: [],
639
- options: {
640
- onSend: () => {
641
- throw new Error('onSend boom');
642
- }
643
- }
644
- });
645
-
646
- test.equal(written.length, 1, 'the command was written to the socket');
647
- test.equal(written[0].toString(), 'A1 NOOP\r\n', 'the actual wire bytes went out before onSend ran');
648
- test.equal(client.currentRequest.sent, true, 'the request is marked as sent');
649
- test.ok(
650
- warnings.some(entry => entry && entry.err && entry.err.message === 'onSend boom'),
651
- 'the callback error was logged'
652
- );
653
- test.done();
654
- };
655
-
656
- // ============================================================================
657
- // countUnknownTag teardown guard
658
- // ============================================================================
659
-
660
- module.exports['Internals: countUnknownTag ignores tags on a closed connection'] = test => {
661
- let client = makeClient();
662
- client.isClosed = true;
663
- client.countUnknownTag('A1');
664
- test.equal(client._unknownTagCount, 0, 'teardown crossover is not counted');
665
- test.done();
666
- };
667
-
668
- // ============================================================================
669
- // rejectUnparsedCompletion
670
- // ============================================================================
671
-
672
- // Installs a command that is current and on the wire, returning a holder for
673
- // the rejection the reader loop would deliver to the caller
674
- const armInFlight = (client, tag) => {
675
- let captured = { err: null };
676
- client.currentRequest = { tag, sent: true };
677
- client.requestTagMap.set(tag, {
678
- tag,
679
- reject: err => {
680
- captured.err = err;
681
- }
682
- });
683
- return captured;
684
- };
685
-
686
- module.exports['Internals: rejectUnparsedCompletion ignores lines without an in-flight command'] = test => {
687
- let client = makeClient();
688
-
689
- client.currentRequest = false;
690
- client.rejectUnparsedCompletion(Buffer.from('A1 OK done'), new Error('parse fail'));
691
- test.equal(client.currentRequest, false, 'no request to settle');
692
-
693
- // A command that is current but not yet on the wire must not be settled either
694
- client.currentRequest = { tag: 'A1', sent: false };
695
- client.rejectUnparsedCompletion(Buffer.from('A1 OK done'), new Error('parse fail'));
696
- test.ok(client.currentRequest, 'the unsent request is untouched');
697
- test.done();
698
- };
699
-
700
- module.exports['Internals: rejectUnparsedCompletion recovers the tag from NUL-padded raw bytes'] = async test => {
701
- let client = makeClient();
702
- let captured = armInFlight(client, 'A1');
703
-
704
- // The parser died before extracting a tag - the raw line carries the buggy-server
705
- // NUL padding, so the fallback must skip it and stop at the first non-tag byte
706
- let parserError = new Error('parse fail');
707
- client.rejectUnparsedCompletion(Buffer.from('\x00\x00A1 \x07garbage'), parserError);
708
-
709
- test.ok(captured.err, 'the in-flight command was failed');
710
- test.equal(captured.err.code, 'ParserError');
711
- test.equal(captured.err.parserError, parserError);
712
- test.equal(client.currentRequest, false, 'the request slot was cleared');
713
- test.done();
714
- };
715
-
716
- module.exports['Internals: rejectUnparsedCompletion ignores a mismatched raw tag'] = test => {
717
- let client = makeClient();
718
- let captured = armInFlight(client, 'A1');
719
-
720
- client.rejectUnparsedCompletion(Buffer.from('A2 NO other'), new Error('parse fail'));
721
-
722
- test.equal(captured.err, null, 'a line for another tag settles nothing');
723
- test.ok(client.currentRequest, 'the in-flight request stays current');
724
- test.done();
725
- };