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,1474 +0,0 @@
1
- 'use strict';
2
-
3
- // End-to-end ImapFlow tests against a scriptable in-process mock IMAP server.
4
- // This exercises the connection lifecycle that pure unit tests cannot reach:
5
- // connect/greeting handling, startSession (CAPABILITY/ID/NAMESPACE/ENABLE),
6
- // authentication, the reader loop (OK/NO/BAD/untagged/continuation handling),
7
- // send/trySend/write, socket handlers, autoidle, locks, logout and close.
8
-
9
- const net = require('net');
10
- const { ImapFlow } = require('../lib/imap-flow');
11
-
12
- // ---------------------------------------------------------------------------
13
- // Mock IMAP server
14
- // ---------------------------------------------------------------------------
15
- // `handlers` maps an uppercase command keyword to (ctx) => void, where ctx
16
- // provides { tag, line, args, write(str), ok(text), no(text), bad(text), socket }.
17
- // Sensible defaults are provided for a full happy-path session; tests override
18
- // individual commands as needed.
19
- const createServer = (options = {}) => {
20
- const capabilities = options.capabilities || 'IMAP4rev1 ID ENABLE NAMESPACE UIDPLUS CONDSTORE MOVE QUOTA';
21
- const greeting = options.greeting || `* OK [CAPABILITY ${capabilities}] mock ready\r\n`;
22
-
23
- const defaults = {
24
- CAPABILITY(ctx) {
25
- ctx.write(`* CAPABILITY ${capabilities}\r\n`);
26
- ctx.ok('CAPABILITY completed');
27
- },
28
- ID(ctx) {
29
- ctx.write('* ID ("name" "mock" "version" "1.0")\r\n');
30
- ctx.ok('ID completed');
31
- },
32
- NAMESPACE(ctx) {
33
- ctx.write('* NAMESPACE (("" "/")) NIL NIL\r\n');
34
- ctx.ok('NAMESPACE completed');
35
- },
36
- ENABLE(ctx) {
37
- ctx.write('* ENABLED CONDSTORE\r\n');
38
- ctx.ok('ENABLE completed');
39
- },
40
- LOGIN(ctx) {
41
- ctx.ok('LOGIN completed');
42
- },
43
- COMPRESS(ctx) {
44
- ctx.no('COMPRESS not available');
45
- },
46
- SELECT(ctx) {
47
- ctx.write('* 3 EXISTS\r\n');
48
- ctx.write('* 0 RECENT\r\n');
49
- ctx.write('* FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)\r\n');
50
- ctx.write('* OK [PERMANENTFLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft \\*)] Limited\r\n');
51
- ctx.write('* OK [UIDVALIDITY 12345] UIDs valid\r\n');
52
- ctx.write('* OK [UIDNEXT 100] Predicted next UID\r\n');
53
- ctx.write('* OK [HIGHESTMODSEQ 1000] Highest\r\n');
54
- ctx.ok('[READ-WRITE] SELECT completed');
55
- },
56
- EXAMINE(ctx) {
57
- ctx.write('* 3 EXISTS\r\n');
58
- ctx.write('* OK [UIDVALIDITY 12345] UIDs valid\r\n');
59
- ctx.write('* OK [UIDNEXT 100] Predicted next UID\r\n');
60
- ctx.ok('[READ-ONLY] EXAMINE completed');
61
- },
62
- LIST(ctx) {
63
- ctx.write('* LIST (\\HasNoChildren) "/" "INBOX"\r\n');
64
- ctx.write('* LIST (\\HasNoChildren \\Sent) "/" "Sent"\r\n');
65
- ctx.ok('LIST completed');
66
- },
67
- LSUB(ctx) {
68
- ctx.write('* LSUB (\\HasNoChildren) "/" "INBOX"\r\n');
69
- ctx.ok('LSUB completed');
70
- },
71
- STATUS(ctx) {
72
- ctx.write('* STATUS "INBOX" (MESSAGES 3 UIDNEXT 100 UIDVALIDITY 12345 UNSEEN 1)\r\n');
73
- ctx.ok('STATUS completed');
74
- },
75
- NOOP(ctx) {
76
- ctx.ok('NOOP completed');
77
- },
78
- LOGOUT(ctx) {
79
- ctx.write('* BYE Logging out\r\n');
80
- ctx.ok('LOGOUT completed');
81
- },
82
- SEARCH(ctx) {
83
- ctx.write('* SEARCH 1 2 3\r\n');
84
- ctx.ok('SEARCH completed');
85
- },
86
- CREATE(ctx) {
87
- ctx.ok('CREATE completed');
88
- },
89
- DELETE(ctx) {
90
- ctx.ok('DELETE completed');
91
- },
92
- RENAME(ctx) {
93
- ctx.ok('RENAME completed');
94
- },
95
- SUBSCRIBE(ctx) {
96
- ctx.ok('SUBSCRIBE completed');
97
- },
98
- UNSUBSCRIBE(ctx) {
99
- ctx.ok('UNSUBSCRIBE completed');
100
- }
101
- };
102
-
103
- const handlers = Object.assign({}, defaults, options.handlers || {});
104
-
105
- const server = net.createServer(socket => {
106
- socket.setNoDelay(true);
107
- socket.on('error', () => {});
108
- if (options.onConnect) {
109
- options.onConnect(socket);
110
- }
111
- if (greeting) {
112
- socket.write(greeting);
113
- }
114
-
115
- let buf = Buffer.alloc(0);
116
- let literalRemaining = 0;
117
- let cmdPrefix = '';
118
-
119
- const dispatch = fullLine => {
120
- // fullLine is the first physical line of the command (tag + command + args);
121
- // literal payloads are not needed to choose a response.
122
- let parts = fullLine.split(' ');
123
- let tag = parts[0];
124
- let command = (parts[1] || '').toUpperCase();
125
- // IDLE is terminated by a bare, untagged "DONE" continuation line, so it
126
- // has no tag/command split — route it to the DONE handler explicitly.
127
- if (!parts[1] && (parts[0] || '').toUpperCase() === 'DONE') {
128
- command = 'DONE';
129
- }
130
- let args = parts.slice(2).join(' ');
131
-
132
- const ctx = {
133
- tag,
134
- command,
135
- line: fullLine,
136
- args,
137
- socket,
138
- write: str => socket.write(str),
139
- ok: text => socket.write(`${tag} OK ${text || 'completed'}\r\n`),
140
- no: text => socket.write(`${tag} NO ${text || 'failed'}\r\n`),
141
- bad: text => socket.write(`${tag} BAD ${text || 'bad'}\r\n`)
142
- };
143
-
144
- let handler = handlers[command];
145
- if (typeof handler === 'function') {
146
- handler(ctx);
147
- } else if (handler === null) {
148
- // explicitly silent (e.g. simulate no response)
149
- } else {
150
- ctx.bad(`Unknown command ${command}`);
151
- }
152
- };
153
-
154
- const processBuffer = () => {
155
- // Loop until we run out of complete lines / literal data.
156
- while (true) {
157
- if (literalRemaining > 0) {
158
- if (buf.length < literalRemaining) {
159
- return;
160
- }
161
- buf = buf.slice(literalRemaining);
162
- literalRemaining = 0;
163
- // Fall through to read the continuation line (post-literal text + CRLF)
164
- }
165
-
166
- let idx = buf.indexOf('\r\n');
167
- if (idx < 0) {
168
- return;
169
- }
170
- let line = buf.slice(0, idx).toString('binary');
171
- buf = buf.slice(idx + 2);
172
-
173
- let combined = cmdPrefix + line;
174
-
175
- // Synchronizing / non-synchronizing literal at end of line?
176
- let m = combined.match(/\{(\d+)(\+)?\}$/);
177
- if (m) {
178
- literalRemaining = Number(m[1]);
179
- // Keep the prefix (sans literal marker) for command keyword extraction
180
- cmdPrefix = combined.replace(/\{(\d+)(\+)?\}$/, '<literal> ');
181
- if (!m[2]) {
182
- // synchronizing literal -> tell client to proceed
183
- socket.write('+ Ready for literal data\r\n');
184
- }
185
- continue;
186
- }
187
-
188
- cmdPrefix = '';
189
- dispatch(combined);
190
- }
191
- };
192
-
193
- socket.on('data', chunk => {
194
- buf = Buffer.concat([buf, chunk]);
195
- processBuffer();
196
- });
197
- });
198
-
199
- return server;
200
- };
201
-
202
- const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve(server.address().port)));
203
-
204
- const makeClient = (port, overrides = {}) =>
205
- new ImapFlow({
206
- host: '127.0.0.1',
207
- port,
208
- secure: false,
209
- disableAutoIdle: true,
210
- disableCompression: true,
211
- logger: false,
212
- auth: { user: 'test', pass: 'secret' },
213
- ...overrides
214
- });
215
-
216
- // ---------------------------------------------------------------------------
217
- // Tests
218
- // ---------------------------------------------------------------------------
219
-
220
- module.exports['Server: full connect + session + logout'] = async test => {
221
- let server = createServer();
222
- let port = await listen(server);
223
- let client = makeClient(port);
224
- client.on('error', () => {});
225
-
226
- await client.connect();
227
- test.ok(client.authenticated, 'authenticated');
228
- test.ok(client.usable, 'usable');
229
- test.ok(client.capabilities.has('IMAP4rev1'));
230
- test.ok(client.serverInfo || client.namespace, 'namespace set');
231
-
232
- await client.logout();
233
- test.equal(client.state, client.states.LOGOUT);
234
-
235
- client.close();
236
- server.close();
237
- test.done();
238
- };
239
-
240
- module.exports['Server: qresync option adds QRESYNC to ENABLE'] = async test => {
241
- let enabledArgs = null;
242
- let server = createServer({
243
- capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE CONDSTORE QRESYNC',
244
- handlers: {
245
- ENABLE(ctx) {
246
- enabledArgs = ctx.args;
247
- ctx.write('* ENABLED CONDSTORE QRESYNC\r\n');
248
- ctx.ok('ENABLE completed');
249
- }
250
- }
251
- });
252
- let port = await listen(server);
253
- let client = makeClient(port, { qresync: true });
254
- client.on('error', () => {});
255
-
256
- await client.connect();
257
- test.ok(/QRESYNC/.test(enabledArgs || ''), 'QRESYNC requested in ENABLE');
258
-
259
- await client.logout();
260
- client.close();
261
- server.close();
262
- test.done();
263
- };
264
-
265
- module.exports['Server: connect runs NOOP and LIST'] = async test => {
266
- let server = createServer();
267
- let port = await listen(server);
268
- let client = makeClient(port);
269
- client.on('error', () => {});
270
-
271
- await client.connect();
272
- await client.noop();
273
- let folders = await client.list();
274
- test.ok(folders.length >= 2);
275
-
276
- await client.logout();
277
- client.close();
278
- server.close();
279
- test.done();
280
- };
281
-
282
- module.exports['Server: enable compression negotiation (server declines)'] = async test => {
283
- let server = createServer();
284
- let port = await listen(server);
285
- // disableCompression false -> client issues COMPRESS, server says NO
286
- let client = makeClient(port, { disableCompression: false });
287
- client.on('error', () => {});
288
-
289
- await client.connect();
290
- test.ok(client.usable);
291
- test.ok(!client._deflate, 'compression not enabled when server declines');
292
-
293
- await client.logout();
294
- client.close();
295
- server.close();
296
- test.done();
297
- };
298
-
299
- module.exports['Server: AUTHENTICATE LOGIN flow'] = async test => {
300
- let server = createServer({
301
- capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE AUTH=LOGIN',
302
- handlers: {
303
- AUTHENTICATE(ctx) {
304
- // SASL LOGIN: server prompts for username then password
305
- ctx.write('+ VXNlcm5hbWU6\r\n'); // "Username:"
306
- ctx.socket.once('data', () => {
307
- ctx.write('+ UGFzc3dvcmQ6\r\n'); // "Password:"
308
- ctx.socket.once('data', () => {
309
- ctx.ok('AUTHENTICATE completed');
310
- });
311
- });
312
- }
313
- }
314
- });
315
- let port = await listen(server);
316
- let client = makeClient(port);
317
- client.on('error', () => {});
318
-
319
- await client.connect();
320
- test.ok(client.authenticated);
321
-
322
- await client.logout();
323
- client.close();
324
- server.close();
325
- test.done();
326
- };
327
-
328
- module.exports['Server: SELECT, SEARCH, STORE and EXPUNGE via run'] = async test => {
329
- let server = createServer({
330
- handlers: {
331
- STORE(ctx) {
332
- ctx.write('* 1 FETCH (FLAGS (\\Seen))\r\n');
333
- ctx.ok('STORE completed');
334
- },
335
- EXPUNGE(ctx) {
336
- ctx.write('* 1 EXPUNGE\r\n');
337
- ctx.ok('EXPUNGE completed');
338
- }
339
- }
340
- });
341
- let port = await listen(server);
342
- let client = makeClient(port);
343
- client.on('error', () => {});
344
-
345
- await client.connect();
346
- let mailbox = await client.mailboxOpen('INBOX');
347
- test.equal(mailbox.exists, 3);
348
-
349
- let found = await client.search({ seen: true }, { uid: false });
350
- test.deepEqual(found, [1, 2, 3]);
351
-
352
- let stored = await client.messageFlagsAdd('1', ['\\Seen']);
353
- test.ok(stored);
354
-
355
- let expungeEvents = [];
356
- client.on('expunge', e => expungeEvents.push(e));
357
- await client.messageDelete('1', { uid: false });
358
- test.ok(expungeEvents.length >= 1);
359
-
360
- await client.logout();
361
- client.close();
362
- server.close();
363
- test.done();
364
- };
365
-
366
- module.exports['Server: FETCH via fetchOne'] = async test => {
367
- let server = createServer({
368
- handlers: {
369
- FETCH(ctx) {
370
- ctx.write('* 1 FETCH (UID 11 FLAGS (\\Seen) RFC822.SIZE 42)\r\n');
371
- ctx.ok('FETCH completed');
372
- }
373
- }
374
- });
375
- let port = await listen(server);
376
- let client = makeClient(port);
377
- client.on('error', () => {});
378
-
379
- await client.connect();
380
- await client.mailboxOpen('INBOX');
381
- let msg = await client.fetchOne('1', { uid: true, flags: true, size: true });
382
- test.equal(msg.uid, 11);
383
- test.equal(msg.size, 42);
384
- test.ok(msg.flags.has('\\Seen'));
385
-
386
- await client.logout();
387
- client.close();
388
- server.close();
389
- test.done();
390
- };
391
-
392
- module.exports['Server: APPEND with synchronizing literal'] = async test => {
393
- let server = createServer({
394
- capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE UIDPLUS',
395
- handlers: {
396
- APPEND(ctx) {
397
- ctx.ok('[APPENDUID 12345 9] APPEND completed');
398
- }
399
- }
400
- });
401
- let port = await listen(server);
402
- let client = makeClient(port);
403
- client.on('error', () => {});
404
-
405
- await client.connect();
406
- let res = await client.append('INBOX', 'Subject: hi\r\n\r\nbody', ['\\Seen']);
407
- test.ok(res);
408
- test.equal(res.uid, 9);
409
-
410
- await client.logout();
411
- client.close();
412
- server.close();
413
- test.done();
414
- };
415
-
416
- module.exports['Server: AUTHENTICATE PLAIN flow'] = async test => {
417
- let server = createServer({
418
- capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE AUTH=PLAIN',
419
- handlers: {
420
- AUTHENTICATE(ctx) {
421
- ctx.write('+ \r\n');
422
- ctx.socket.once('data', () => ctx.ok('AUTHENTICATE completed'));
423
- }
424
- }
425
- });
426
- let port = await listen(server);
427
- let client = makeClient(port);
428
- client.on('error', () => {});
429
-
430
- await client.connect();
431
- test.ok(client.authenticated);
432
-
433
- await client.logout();
434
- client.close();
435
- server.close();
436
- test.done();
437
- };
438
-
439
- module.exports['Server: AUTHENTICATE XOAUTH2 with accessToken'] = async test => {
440
- let server = createServer({
441
- capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE AUTH=XOAUTH2',
442
- handlers: {
443
- AUTHENTICATE(ctx) {
444
- ctx.ok('AUTHENTICATE completed');
445
- }
446
- }
447
- });
448
- let port = await listen(server);
449
- let client = makeClient(port, { auth: { user: 'test', accessToken: 'token-123' } });
450
- client.on('error', () => {});
451
-
452
- await client.connect();
453
- test.ok(client.authenticated);
454
-
455
- await client.logout();
456
- client.close();
457
- server.close();
458
- test.done();
459
- };
460
-
461
- module.exports['Server: backslash username forces LOGIN method'] = async test => {
462
- let loginUsed = false;
463
- let server = createServer({
464
- capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE AUTH=PLAIN',
465
- handlers: {
466
- LOGIN(ctx) {
467
- loginUsed = true;
468
- ctx.ok('LOGIN completed');
469
- }
470
- }
471
- });
472
- let port = await listen(server);
473
- let client = makeClient(port, { auth: { user: 'domain\\user', pass: 'secret' } });
474
- client.on('error', () => {});
475
-
476
- await client.connect();
477
- test.ok(client.authenticated);
478
- test.ok(loginUsed, 'used LOGIN command despite AUTH=PLAIN');
479
-
480
- await client.logout();
481
- client.close();
482
- server.close();
483
- test.done();
484
- };
485
-
486
- module.exports['Server: LOGINDISABLED rejects connect'] = async test => {
487
- let server = createServer({
488
- capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE LOGINDISABLED'
489
- });
490
- let port = await listen(server);
491
- let client = makeClient(port);
492
- client.on('error', () => {});
493
-
494
- let err = null;
495
- try {
496
- await client.connect();
497
- } catch (e) {
498
- err = e;
499
- }
500
- test.ok(err, 'connect rejected when login disabled');
501
-
502
- client.close();
503
- server.close();
504
- test.done();
505
- };
506
-
507
- module.exports['Server: missing auth config rejects connect'] = async test => {
508
- let server = createServer();
509
- let port = await listen(server);
510
- let client = makeClient(port, { auth: false });
511
- client.on('error', () => {});
512
-
513
- let err = null;
514
- try {
515
- await client.connect();
516
- } catch (e) {
517
- err = e;
518
- }
519
- test.ok(err, 'connect rejected without auth');
520
-
521
- client.close();
522
- server.close();
523
- test.done();
524
- };
525
-
526
- module.exports['Server: auth without password rejects connect'] = async test => {
527
- let server = createServer();
528
- let port = await listen(server);
529
- let client = makeClient(port, { auth: { user: 'only-user' } });
530
- client.on('error', () => {});
531
-
532
- let err = null;
533
- try {
534
- await client.connect();
535
- } catch (e) {
536
- err = e;
537
- }
538
- test.ok(err, 'connect rejected without password');
539
-
540
- client.close();
541
- server.close();
542
- test.done();
543
- };
544
-
545
- module.exports['Server: command returning NO rejects with responseStatus'] = async test => {
546
- let server = createServer({
547
- handlers: {
548
- CREATE(ctx) {
549
- ctx.no('Mailbox already exists');
550
- }
551
- }
552
- });
553
- let port = await listen(server);
554
- let client = makeClient(port);
555
- client.on('error', () => {});
556
-
557
- await client.connect();
558
- let err = null;
559
- try {
560
- await client.mailboxCreate('Existing');
561
- } catch (e) {
562
- err = e;
563
- }
564
- test.ok(err);
565
- test.equal(err.responseStatus, 'NO');
566
-
567
- await client.logout();
568
- client.close();
569
- server.close();
570
- test.done();
571
- };
572
-
573
- module.exports['Server: command returning BAD rejects'] = async test => {
574
- let server = createServer({
575
- handlers: {
576
- CREATE(ctx) {
577
- ctx.bad('Invalid arguments');
578
- }
579
- }
580
- });
581
- let port = await listen(server);
582
- let client = makeClient(port);
583
- client.on('error', () => {});
584
-
585
- await client.connect();
586
- let err = null;
587
- try {
588
- await client.mailboxCreate('Whatever');
589
- } catch (e) {
590
- err = e;
591
- }
592
- test.ok(err);
593
- test.equal(err.responseStatus, 'BAD');
594
-
595
- await client.logout();
596
- client.close();
597
- server.close();
598
- test.done();
599
- };
600
-
601
- module.exports['Server: verifyOnly connect lists mailboxes and logs out'] = async test => {
602
- let server = createServer();
603
- let port = await listen(server);
604
- let client = makeClient(port, { verifyOnly: true, includeMailboxes: true });
605
- client.on('error', () => {});
606
-
607
- await client.connect();
608
- test.ok(Array.isArray(client._mailboxList), 'mailbox list captured');
609
- test.ok(client._mailboxList.length >= 1);
610
- // verifyOnly logs out at the end of startSession
611
- test.equal(client.state, client.states.LOGOUT);
612
-
613
- client.close();
614
- server.close();
615
- test.done();
616
- };
617
-
618
- module.exports['Server: verifyOnly keeps the authentication result after logging out'] = async test => {
619
- // The whole point of verifyOnly is to report whether the credentials work. The mode logs out
620
- // before connect() resolves, so if close() cleared `authenticated` the caller would read false
621
- // off a connection that had just authenticated, with no later moment to read the real answer.
622
- let server = createServer();
623
- let port = await listen(server);
624
- let client = makeClient(port, { verifyOnly: true, includeMailboxes: true });
625
- client.on('error', () => {});
626
-
627
- await client.connect();
628
- test.ok(client.authenticated, 'authentication result survives the verifyOnly logout');
629
-
630
- client.close();
631
- test.ok(client.authenticated, 'and survives an explicit close as well');
632
-
633
- server.close();
634
- test.done();
635
- };
636
-
637
- module.exports['Server: an ordinary session clears the authentication state on close'] = async test => {
638
- // The counterpart: for a session that is not verifyOnly, `authenticated` describes live state
639
- // and must not survive the connection, or reconnect logic reads it as still signed in.
640
- let server = createServer();
641
- let port = await listen(server);
642
- let client = makeClient(port);
643
- client.on('error', () => {});
644
-
645
- await client.connect();
646
- test.ok(client.authenticated, 'authenticated while the session is up');
647
-
648
- client.close();
649
- test.equal(client.authenticated, false, 'cleared once the session ends');
650
-
651
- server.close();
652
- test.done();
653
- };
654
-
655
- module.exports['Server: ID is re-requested after login when first response is sparse'] = async test => {
656
- let idCalls = 0;
657
- let server = createServer({
658
- handlers: {
659
- ID(ctx) {
660
- idCalls++;
661
- if (idCalls === 1) {
662
- // sparse/NIL ID before login triggers a re-request afterwards
663
- ctx.write('* ID NIL\r\n');
664
- } else {
665
- ctx.write('* ID ("name" "mock" "version" "1")\r\n');
666
- }
667
- ctx.ok('ID completed');
668
- }
669
- }
670
- });
671
- let port = await listen(server);
672
- let client = makeClient(port);
673
- client.on('error', () => {});
674
-
675
- await client.connect();
676
- test.ok(idCalls >= 2, 'ID requested again after login');
677
-
678
- await client.logout();
679
- client.close();
680
- server.close();
681
- test.done();
682
- };
683
-
684
- module.exports['Server: NAMESPACE BAD with auth message surfaces auth failure'] = async test => {
685
- let server = createServer({
686
- handlers: {
687
- NAMESPACE(ctx) {
688
- ctx.bad('User is authenticated but not connected');
689
- }
690
- }
691
- });
692
- let port = await listen(server);
693
- let client = makeClient(port);
694
- client.on('error', () => {});
695
-
696
- let err = null;
697
- try {
698
- await client.connect();
699
- } catch (e) {
700
- err = e;
701
- }
702
- test.ok(err, 'connect rejected');
703
- test.ok(/Authentication failed/i.test(err.message) || err.authenticationFailed, 'reported as auth failure');
704
-
705
- client.close();
706
- server.close();
707
- test.done();
708
- };
709
-
710
- module.exports['Server: getMailboxLock selects and releases'] = async test => {
711
- let server = createServer();
712
- let port = await listen(server);
713
- let client = makeClient(port);
714
- client.on('error', () => {});
715
-
716
- await client.connect();
717
- let lock = await client.getMailboxLock('INBOX');
718
- test.equal(client.mailbox.path, 'INBOX');
719
- lock.release();
720
-
721
- // second lock on same mailbox -> fast path
722
- let lock2 = await client.getMailboxLock('INBOX');
723
- lock2.release();
724
-
725
- await client.logout();
726
- client.close();
727
- server.close();
728
- test.done();
729
- };
730
-
731
- module.exports['Server: getMailboxLock rejects for missing mailbox'] = async test => {
732
- let server = createServer({
733
- handlers: {
734
- SELECT(ctx) {
735
- ctx.no('Mailbox does not exist');
736
- },
737
- LIST(ctx) {
738
- // empty LIST -> mailbox confirmed missing
739
- ctx.ok('LIST completed');
740
- }
741
- }
742
- });
743
- let port = await listen(server);
744
- let client = makeClient(port);
745
- client.on('error', () => {});
746
-
747
- await client.connect();
748
- let err = null;
749
- try {
750
- await client.getMailboxLock('Missing');
751
- } catch (e) {
752
- err = e;
753
- }
754
- test.ok(err);
755
- test.ok(err.mailboxMissing, 'flagged as missing mailbox');
756
-
757
- client.close();
758
- server.close();
759
- test.done();
760
- };
761
-
762
- module.exports['Server: IDLE then break out'] = async test => {
763
- test.expect(2);
764
- let idleTag = null;
765
- let doneReceived = false;
766
- // Capabilities MUST advertise IDLE, otherwise the client falls back to NOOP polling
767
- // and never sends an IDLE command at all.
768
- let server = createServer({
769
- capabilities: 'IMAP4rev1 IDLE ID ENABLE NAMESPACE UIDPLUS CONDSTORE MOVE QUOTA',
770
- handlers: {
771
- IDLE(ctx) {
772
- // Remember the IDLE command tag so the matching DONE can complete it.
773
- idleTag = ctx.tag;
774
- ctx.write('+ idling\r\n');
775
- },
776
- DONE(ctx) {
777
- doneReceived = true;
778
- // Complete the original IDLE command so client.idle() resolves cleanly.
779
- ctx.socket.write(`${idleTag} OK IDLE terminated\r\n`);
780
- }
781
- }
782
- });
783
- let port = await listen(server);
784
- let client = makeClient(port);
785
- client.on('error', () => {});
786
-
787
- await client.connect();
788
- await client.mailboxOpen('INBOX');
789
-
790
- // Enter IDLE, then break out by issuing another command. Queuing NOOP triggers
791
- // preCheck(), which sends DONE; the server's DONE handler then completes IDLE.
792
- let idlePromise = client.idle();
793
- await client.noop();
794
- await idlePromise;
795
-
796
- test.ok(doneReceived, 'server received the DONE continuation');
797
- test.equal(client.idling, false, 'client left the IDLE state');
798
-
799
- client.close();
800
- server.close();
801
- test.done();
802
- };
803
-
804
- module.exports['Server: stats counts bytes'] = async test => {
805
- let server = createServer();
806
- let port = await listen(server);
807
- let client = makeClient(port);
808
- client.on('error', () => {});
809
-
810
- await client.connect();
811
- let stats = client.stats();
812
- test.ok(stats.sent > 0);
813
- test.ok(stats.received > 0);
814
-
815
- let stats2 = client.stats(true); // reset
816
- test.ok(stats2.sent >= 0);
817
- let stats3 = client.stats();
818
- test.equal(stats3.sent, 0);
819
-
820
- await client.logout();
821
- client.close();
822
- server.close();
823
- test.done();
824
- };
825
-
826
- module.exports['Server: PREAUTH greeting skips login'] = async test => {
827
- let server = createServer({
828
- greeting: '* PREAUTH [CAPABILITY IMAP4rev1 ID ENABLE NAMESPACE] already authenticated\r\n'
829
- });
830
- let port = await listen(server);
831
- let client = makeClient(port);
832
- client.on('error', () => {});
833
-
834
- await client.connect();
835
- test.equal(client.state, client.states.AUTHENTICATED);
836
- test.ok(client.usable);
837
- // documented contract: `true` if the connection was authenticated by PREAUTH
838
- test.strictEqual(client.authenticated, true);
839
-
840
- await client.logout();
841
- client.close();
842
- server.close();
843
- test.done();
844
- };
845
-
846
- module.exports['Server: unsolicited EXISTS and VANISHED reach the untagged handlers'] = async test => {
847
- let server = createServer({
848
- capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE QRESYNC',
849
- handlers: {
850
- NOOP(ctx) {
851
- // unsolicited untagged updates piggybacked on NOOP
852
- ctx.write('* 5 EXISTS\r\n');
853
- ctx.write('* VANISHED 1:2\r\n');
854
- ctx.ok('NOOP completed');
855
- }
856
- }
857
- });
858
- let port = await listen(server);
859
- let client = makeClient(port);
860
- client.on('error', () => {});
861
-
862
- await client.connect();
863
- await client.mailboxOpen('INBOX');
864
-
865
- let existsEvent = null;
866
- let expungeEvents = [];
867
- client.on('exists', e => {
868
- existsEvent = e;
869
- });
870
- client.on('expunge', e => expungeEvents.push(e));
871
-
872
- await client.noop();
873
- // allow the untagged handlers to run
874
- await new Promise(r => setTimeout(r, 20));
875
-
876
- test.ok(existsEvent, 'EXISTS handler fired');
877
- test.equal(existsEvent.count, 5);
878
- test.ok(expungeEvents.length >= 1, 'VANISHED handler fired');
879
- test.equal(expungeEvents[0].vanished, true);
880
-
881
- await client.logout();
882
- client.close();
883
- server.close();
884
- test.done();
885
- };
886
-
887
- module.exports['Server: rev2-shaped SELECT response is tolerated through the full pipeline'] = async test => {
888
- // RFC 9051 requires rev2 servers to include an untagged LIST in the SELECT
889
- // response and a CLOSED response code when another mailbox was selected, and
890
- // to omit RECENT/UNSEEN. The client must consume such a response cleanly.
891
- let server = createServer({
892
- capabilities: 'IMAP4rev2 ID ENABLE NAMESPACE',
893
- handlers: {
894
- SELECT(ctx) {
895
- ctx.write('* OK [CLOSED] Previous mailbox closed\r\n');
896
- ctx.write('* 3 EXISTS\r\n');
897
- ctx.write('* LIST () "/" INBOX\r\n');
898
- ctx.write('* FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)\r\n');
899
- ctx.write('* OK [PERMANENTFLAGS (\\Seen \\*)] Limited\r\n');
900
- ctx.write('* OK [UIDVALIDITY 12345] UIDs valid\r\n');
901
- ctx.write('* OK [UIDNEXT 100] Predicted next UID\r\n');
902
- ctx.ok('[READ-WRITE] SELECT completed');
903
- }
904
- }
905
- });
906
- let port = await listen(server);
907
- let client = makeClient(port);
908
- let errors = [];
909
- client.on('error', err => errors.push(err));
910
-
911
- await client.connect();
912
- test.ok(client.capabilities.has('IMAP4rev2'));
913
-
914
- let mailbox = await client.mailboxOpen('INBOX');
915
- test.equal(mailbox.path, 'INBOX');
916
- test.equal(mailbox.exists, 3);
917
- test.equal(mailbox.uidNext, 100);
918
- test.equal(mailbox.uidValidity, 12345n);
919
-
920
- // re-select drives the CLOSED response code through the live pipeline again
921
- let again = await client.mailboxOpen('INBOX');
922
- test.equal(again.exists, 3);
923
- test.equal(errors.length, 0, 'no errors from the rev2-shaped response');
924
-
925
- await client.logout();
926
- client.close();
927
- server.close();
928
- test.done();
929
- };
930
-
931
- module.exports['Server: unsolicited STATUS for another mailbox is tolerated'] = async test => {
932
- // RFC 9051 (Appendix E item 20): with rev2, servers may push updates that are
933
- // unrelated to the selected mailbox (e.g. a STATUS for another mailbox during
934
- // IDLE). The client must ignore them without corrupting the selected state.
935
- let server = createServer({
936
- handlers: {
937
- NOOP(ctx) {
938
- ctx.write('* STATUS "Other" (MESSAGES 5 UNSEEN 1)\r\n');
939
- ctx.ok('NOOP completed');
940
- }
941
- }
942
- });
943
- let port = await listen(server);
944
- let client = makeClient(port);
945
- let errors = [];
946
- client.on('error', err => errors.push(err));
947
-
948
- await client.connect();
949
- await client.mailboxOpen('INBOX');
950
- test.equal(client.mailbox.exists, 3);
951
-
952
- await client.noop();
953
- await new Promise(r => setTimeout(r, 20));
954
-
955
- test.equal(client.mailbox.path, 'INBOX', 'selected mailbox unchanged');
956
- test.equal(client.mailbox.exists, 3, 'selected mailbox message count unchanged');
957
- test.equal(errors.length, 0, 'unsolicited STATUS must not raise errors');
958
-
959
- await client.logout();
960
- client.close();
961
- server.close();
962
- test.done();
963
- };
964
-
965
- module.exports['Server: invalid tagged response rejects with InvalidResponse'] = async test => {
966
- let server = createServer({
967
- handlers: {
968
- CREATE(ctx) {
969
- // a tagged response whose command is neither OK/NO/BAD
970
- ctx.write(`${ctx.tag} WEIRD unexpected status\r\n`);
971
- }
972
- }
973
- });
974
- let port = await listen(server);
975
- let client = makeClient(port);
976
- client.on('error', () => {});
977
-
978
- await client.connect();
979
- let err = null;
980
- try {
981
- await client.mailboxCreate('Whatever');
982
- } catch (e) {
983
- err = e;
984
- }
985
- test.ok(err);
986
- test.equal(err.code, 'InvalidResponse');
987
-
988
- client.close();
989
- server.close();
990
- test.done();
991
- };
992
-
993
- module.exports['Server: reader tolerates an unparseable untagged line'] = async test => {
994
- let server = createServer({
995
- handlers: {
996
- NOOP(ctx) {
997
- // malformed untagged line that the parser cannot parse, then a valid OK
998
- ctx.write('* 1 FETCH (]\r\n');
999
- ctx.ok('NOOP completed');
1000
- }
1001
- }
1002
- });
1003
- let port = await listen(server);
1004
- let client = makeClient(port);
1005
- client.on('error', () => {});
1006
-
1007
- await client.connect();
1008
- // The malformed line is logged & skipped; NOOP still resolves
1009
- await client.noop();
1010
- test.ok(true);
1011
-
1012
- await client.logout();
1013
- client.close();
1014
- server.close();
1015
- test.done();
1016
- };
1017
-
1018
- module.exports['Server: reader yields to event loop on many untagged responses'] = async test => {
1019
- let server = createServer({
1020
- handlers: {
1021
- STORE(ctx) {
1022
- // 12 untagged FETCH responses + OK in a SINGLE write. STORE's FETCH
1023
- // responses go through the (non-backpressured) global handler, so the
1024
- // reader drains all >10 items in one pass and hits the periodic yield.
1025
- let out = '';
1026
- for (let i = 1; i <= 12; i++) {
1027
- out += `* ${i} FETCH (FLAGS (\\Seen))\r\n`;
1028
- }
1029
- out += `${ctx.tag} OK STORE completed\r\n`;
1030
- ctx.write(out);
1031
- }
1032
- }
1033
- });
1034
- let port = await listen(server);
1035
- let client = makeClient(port);
1036
- client.on('error', () => {});
1037
-
1038
- await client.connect();
1039
- await client.mailboxOpen('INBOX');
1040
- let ok = await client.messageFlagsAdd('1:12', ['\\Seen']);
1041
- test.ok(ok);
1042
-
1043
- await client.logout();
1044
- client.close();
1045
- server.close();
1046
- test.done();
1047
- };
1048
-
1049
- module.exports['Server: partial FETCH NO is treated as success'] = async test => {
1050
- let server = createServer({
1051
- handlers: {
1052
- FETCH(ctx) {
1053
- ctx.write('* 1 FETCH (UID 1 FLAGS (\\Seen))\r\n');
1054
- ctx.no('Some of the requested messages no longer exist');
1055
- }
1056
- }
1057
- });
1058
- let port = await listen(server);
1059
- let client = makeClient(port);
1060
- client.on('error', () => {});
1061
-
1062
- await client.connect();
1063
- await client.mailboxOpen('INBOX');
1064
- // The NO with this specific text is treated as success rather than rejecting
1065
- let msg = await client.fetchOne('1', { uid: true });
1066
- test.ok(msg);
1067
- test.equal(msg.uid, 1);
1068
-
1069
- await client.logout();
1070
- client.close();
1071
- server.close();
1072
- test.done();
1073
- };
1074
-
1075
- // NB: the socket-timeout -> NOOP/IDLE recovery handler is covered deterministically
1076
- // in imap-flow-coverage-test.js (the _socketTimeout tests) rather than via a flaky
1077
- // real-clock socket timeout here.
1078
-
1079
- module.exports['Server: greeting timeout rejects connect'] = async test => {
1080
- // Server accepts the socket but never sends a greeting
1081
- let server = net.createServer(socket => {
1082
- socket.on('error', () => {});
1083
- // intentionally silent
1084
- });
1085
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
1086
- let port = server.address().port;
1087
-
1088
- let client = makeClient(port, { greetingTimeout: 100 });
1089
- client.on('error', () => {});
1090
-
1091
- let err = null;
1092
- try {
1093
- await client.connect();
1094
- } catch (e) {
1095
- err = e;
1096
- }
1097
- test.ok(err);
1098
- test.equal(err.code, 'GREETING_TIMEOUT');
1099
-
1100
- client.close();
1101
- server.close();
1102
- test.done();
1103
- };
1104
-
1105
- module.exports['Server: connection timeout rejects connect'] = async test => {
1106
- // 192.0.2.0/24 (TEST-NET-1, RFC 5737) is reserved and never answers, so the TCP
1107
- // connect hangs until the (very short) connection timeout fires.
1108
- let client = makeClient(9, { host: '192.0.2.1', connectionTimeout: 120 });
1109
- client.on('error', () => {});
1110
-
1111
- let err = null;
1112
- try {
1113
- await client.connect();
1114
- } catch (e) {
1115
- err = e;
1116
- }
1117
- test.ok(err, 'connect rejected');
1118
- test.equal(err.code, 'CONNECT_TIMEOUT');
1119
-
1120
- client.close();
1121
- test.done();
1122
- };
1123
-
1124
- module.exports['Server: proxy connection failure rejects connect'] = async test => {
1125
- let client = makeClient(1, {
1126
- // point at a port with nothing listening so the proxy setup fails
1127
- proxy: 'socks://127.0.0.1:1'
1128
- });
1129
- client.on('error', () => {});
1130
-
1131
- let err = null;
1132
- try {
1133
- await client.connect();
1134
- } catch (e) {
1135
- err = e;
1136
- }
1137
- test.ok(err, 'connect rejected on proxy failure');
1138
-
1139
- client.close();
1140
- test.done();
1141
- };
1142
-
1143
- module.exports['Server: close clears public session state'] = async test => {
1144
- // Callers inspect these properties in reconnect logic, so they must not keep describing a
1145
- // session that is gone.
1146
- let server = createServer();
1147
- let port = await listen(server);
1148
- let client = makeClient(port);
1149
- client.on('error', () => {});
1150
-
1151
- let events = [];
1152
- client.on('mailboxClose', mailbox => events.push({ event: 'mailboxClose', path: mailbox.path }));
1153
- client.on('close', () => events.push({ event: 'close' }));
1154
-
1155
- await client.connect();
1156
- await client.mailboxOpen('INBOX');
1157
-
1158
- test.ok(client.mailbox, 'a mailbox is selected');
1159
- test.ok(client.authenticated, 'the session is authenticated');
1160
- test.ok(client.currentSelectCommand, 'the select command is remembered for polling');
1161
-
1162
- client.close();
1163
-
1164
- test.equal(client.mailbox, false, 'mailbox state cleared');
1165
- test.equal(client.currentSelectCommand, false, 'saved select command cleared');
1166
- test.equal(client.authenticated, false, 'authentication state cleared');
1167
- test.equal(client.preCheck, false, 'preCheck cleared');
1168
- test.equal(client.usable, false, 'connection no longer usable');
1169
- test.equal(client.idling, false, 'idling cleared');
1170
- test.equal(client.state, client.states.LOGOUT, 'state is LOGOUT');
1171
-
1172
- // The selected mailbox transitions to closed exactly once, before 'close'
1173
- test.deepEqual(events, [{ event: 'mailboxClose', path: 'INBOX' }, { event: 'close' }], 'mailboxClose is emitted once, ahead of close');
1174
-
1175
- // Repeated close() is idempotent and emits nothing more
1176
- client.close();
1177
- client.close();
1178
- test.equal(events.length, 2, 'no duplicate events from repeated close()');
1179
-
1180
- server.close();
1181
- test.done();
1182
- };
1183
-
1184
- module.exports['Server: close without a selected mailbox emits no mailboxClose'] = async test => {
1185
- let server = createServer();
1186
- let port = await listen(server);
1187
- let client = makeClient(port);
1188
- client.on('error', () => {});
1189
-
1190
- let mailboxCloseCount = 0;
1191
- client.on('mailboxClose', () => mailboxCloseCount++);
1192
-
1193
- await client.connect();
1194
- client.close();
1195
-
1196
- test.equal(mailboxCloseCount, 0, 'nothing to close, nothing emitted');
1197
- server.close();
1198
- test.done();
1199
- };
1200
-
1201
- module.exports['Server: GETQUOTA fallback releases the parser'] = async test => {
1202
- // Regression: the GETQUOTA fallback never handed its response back to the reader, so the
1203
- // parser stayed blocked on its backpressure callback and every later command hung.
1204
- let server = createServer({
1205
- capabilities: 'IMAP4rev1 ID ENABLE NAMESPACE QUOTA',
1206
- handlers: {
1207
- GETQUOTAROOT(ctx) {
1208
- // root only, no inline QUOTA response - forces the fallback command
1209
- ctx.write('* QUOTAROOT "INBOX" "userquota"\r\n');
1210
- ctx.ok('GETQUOTAROOT completed');
1211
- },
1212
- GETQUOTA(ctx) {
1213
- ctx.write('* QUOTA "userquota" (STORAGE 512 1024)\r\n');
1214
- ctx.ok('GETQUOTA completed');
1215
- }
1216
- }
1217
- });
1218
- let port = await listen(server);
1219
- let client = makeClient(port);
1220
- client.on('error', () => {});
1221
-
1222
- await client.connect();
1223
-
1224
- let quota = await client.getQuota();
1225
- test.ok(quota, 'quota resolved');
1226
- test.equal(quota.quotaRoot, 'userquota', 'quota root from the first command');
1227
- test.equal(quota.storage.usage, 512 * 1024, 'quota usage from the fallback command');
1228
-
1229
- // The parser must still be live: a following command has to complete.
1230
- let noopResponse = await client.exec('NOOP', false, {});
1231
- noopResponse.next();
1232
- test.ok(noopResponse.response, 'the connection still processes commands after the fallback');
1233
-
1234
- await client.logout();
1235
- client.close();
1236
- server.close();
1237
- test.done();
1238
- };
1239
-
1240
- module.exports['Server: oversized literal cannot inject protocol'] = async test => {
1241
- // Response-injection regression. With a lowered maxLiteralSize the parser rejects the
1242
- // literal, and everything after the marker line is ordinary message content chosen by a
1243
- // third party. None of it may be parsed: no untagged handler may fire and the forged
1244
- // tagged completion may not settle the in-flight request.
1245
- let server = createServer({
1246
- handlers: {
1247
- NOOP(ctx) {
1248
- ctx.write(
1249
- `* 1 FETCH (BODY[] {5000}\r\n` + //
1250
- `INNOCENT MESSAGE TEXT\r\n` +
1251
- `* 9999 EXISTS\r\n` +
1252
- `${ctx.tag} OK forged completion\r\n`
1253
- );
1254
- }
1255
- }
1256
- });
1257
- let port = await listen(server);
1258
- let client = makeClient(port, { maxLiteralSize: 1024 });
1259
-
1260
- let errors = [];
1261
- let existsEvents = [];
1262
- client.on('error', err => errors.push(err));
1263
- client.on('exists', ev => existsEvents.push(ev));
1264
-
1265
- await client.connect();
1266
- let mailbox = await client.mailboxOpen('INBOX');
1267
- test.equal(mailbox.exists, 3, 'mailbox opened with the server reported count');
1268
-
1269
- let noopErr = null;
1270
- try {
1271
- // exec() surfaces the raw command outcome, so a forged tagged completion would show
1272
- // up here as a resolved request (client.noop() would swallow it into `false`).
1273
- await client.exec('NOOP', false, {});
1274
- test.ok(false, 'the forged tagged completion must not resolve the in-flight command');
1275
- } catch (err) {
1276
- noopErr = err;
1277
- }
1278
-
1279
- test.ok(noopErr, 'the in-flight command rejected instead of accepting injected content');
1280
- test.deepEqual(existsEvents, [], 'the injected untagged EXISTS never reached an untagged handler');
1281
- test.ok(
1282
- errors.some(err => err.code === 'LiteralTooLarge'),
1283
- 'the limit violation is surfaced to the caller'
1284
- );
1285
- test.ok(client.isClosed || !client.usable, 'the connection failed closed');
1286
-
1287
- client.close();
1288
- server.close();
1289
- test.done();
1290
- };
1291
-
1292
- module.exports['Server: socket close triggers close handling'] = async test => {
1293
- let server = createServer();
1294
- let port = await listen(server);
1295
- let client = makeClient(port);
1296
- client.on('error', () => {});
1297
-
1298
- await client.connect();
1299
- let closed = false;
1300
- client.on('close', () => {
1301
- closed = true;
1302
- });
1303
-
1304
- // Drop the server side
1305
- server.close();
1306
- client.socket.destroy();
1307
-
1308
- await new Promise(r => setTimeout(r, 60));
1309
- test.ok(closed || client.isClosed, 'connection closed');
1310
- client.close();
1311
- test.done();
1312
- };
1313
-
1314
- module.exports['Server: an unparseable tagged completion fails the command instead of stalling'] = async test => {
1315
- // A tagged line the parser cannot make sense of used to be logged and dropped.
1316
- // currentRequest then stayed set forever, so trySend() stopped dispatching and
1317
- // every later command queued behind a promise that never settled.
1318
- let server = createServer({
1319
- handlers: {
1320
- SELECT(ctx) {
1321
- // A control character inside the response code is not parseable
1322
- ctx.write(`${ctx.tag} OK [\x01BAD-CODE] SELECT completed\r\n`);
1323
- }
1324
- }
1325
- });
1326
- let port = await listen(server);
1327
- let client = makeClient(port);
1328
- client.on('error', () => {});
1329
-
1330
- await client.connect();
1331
-
1332
- let selectErr = null;
1333
- try {
1334
- await client.mailboxOpen('INBOX');
1335
- } catch (err) {
1336
- selectErr = err;
1337
- }
1338
- test.ok(selectErr, 'the command whose completion could not be parsed must reject');
1339
-
1340
- // The connection has to keep working: the next command still gets dispatched
1341
- let folders = await client.list();
1342
- test.ok(Array.isArray(folders) && folders.length, 'later commands still run');
1343
-
1344
- await client.logout();
1345
- client.close();
1346
- server.close();
1347
- test.done();
1348
- };
1349
-
1350
- module.exports['Server: a NUL-padded unparseable completion still fails the command'] = async test => {
1351
- // Buggy servers pad lines with leading NUL bytes; the parser strips them before
1352
- // reading the tag. The unparsed-completion recovery has to see the same tag the
1353
- // parser saw, or the command hangs for exactly the server class the NUL
1354
- // workaround exists for.
1355
- let server = createServer({
1356
- handlers: {
1357
- SELECT(ctx) {
1358
- ctx.write(`\x00\x00${ctx.tag} OK [\x01BAD-CODE] SELECT completed\r\n`);
1359
- }
1360
- }
1361
- });
1362
- let port = await listen(server);
1363
- let client = makeClient(port);
1364
- client.on('error', () => {});
1365
-
1366
- await client.connect();
1367
-
1368
- let selectErr = null;
1369
- try {
1370
- await client.mailboxOpen('INBOX');
1371
- } catch (err) {
1372
- selectErr = err;
1373
- }
1374
- test.ok(selectErr, 'the command whose completion could not be parsed must reject');
1375
-
1376
- let folders = await client.list();
1377
- test.ok(Array.isArray(folders) && folders.length, 'later commands still run');
1378
-
1379
- await client.logout();
1380
- client.close();
1381
- server.close();
1382
- test.done();
1383
- };
1384
-
1385
- module.exports['Server: a sequence-shaped token in a server response does not fail the connection'] = async test => {
1386
- // The incoming token parser accepts sequence-shaped tokens the strict outgoing
1387
- // grammar rejects ("1:2:3"). Every parsed response is re-compiled for the log, so
1388
- // that pass must skip the validation - one quirky but parseable server line would
1389
- // otherwise tear down the whole connection.
1390
- let server = createServer({
1391
- handlers: {
1392
- NOOP(ctx) {
1393
- ctx.write(`${ctx.tag} OK [XDATA 1:2:3] NOOP completed\r\n`);
1394
- }
1395
- }
1396
- });
1397
- let port = await listen(server);
1398
- let client = makeClient(port);
1399
- client.on('error', () => {});
1400
-
1401
- await client.connect();
1402
- await client.noop();
1403
- test.ok(client.usable, 'connection must stay usable');
1404
-
1405
- let folders = await client.list();
1406
- test.ok(Array.isArray(folders) && folders.length, 'later commands still run');
1407
-
1408
- await client.logout();
1409
- client.close();
1410
- server.close();
1411
- test.done();
1412
- };
1413
-
1414
- module.exports['Server: an invalid range rejects the command without wedging the queue'] = async test => {
1415
- // The compiler refuses invalid sequence sets before anything reaches the wire.
1416
- // The dispatch layer has to treat that as the command's own failure: the request
1417
- // must reject (even when queued behind an in-flight command) and the queue must
1418
- // keep moving instead of waiting forever on a response that can never arrive.
1419
- let server = createServer();
1420
- let port = await listen(server);
1421
- let client = makeClient(port);
1422
- client.on('error', () => {});
1423
-
1424
- await client.connect();
1425
- await client.mailboxOpen('INBOX');
1426
-
1427
- let err = null;
1428
- try {
1429
- await client.fetchOne('1;2', { uid: true });
1430
- } catch (e) {
1431
- err = e;
1432
- }
1433
- test.ok(err, 'the invalid range must reject');
1434
- test.equal(err && err.code, 'InvalidSequenceSet');
1435
-
1436
- // Queued variant: the invalid command sits behind an in-flight one; every promise
1437
- // must settle and the command behind the invalid one must still run
1438
- let results = await Promise.allSettled([client.noop(), client.fetchOne('3;4', { uid: true }), client.noop()]);
1439
- test.equal(results[0].status, 'fulfilled', 'command before the invalid one succeeds');
1440
- test.equal(results[1].status, 'rejected', 'queued invalid command must reject, not hang');
1441
- test.equal(results[2].status, 'fulfilled', 'command after the invalid one still runs');
1442
-
1443
- await client.logout();
1444
- client.close();
1445
- server.close();
1446
- test.done();
1447
- };
1448
-
1449
- module.exports['Server: a throwing response listener does not fail the command'] = async test => {
1450
- // 'response' is emitted after a tagged completion parses successfully. A listener
1451
- // throwing synchronously used to be caught by the parse-failure path, which
1452
- // rejected the in-flight command with a bogus ParserError even though the server
1453
- // had executed it.
1454
- let server = createServer();
1455
- let port = await listen(server);
1456
- let client = makeClient(port);
1457
- client.on('error', () => {});
1458
-
1459
- await client.connect();
1460
- client.on('response', payload => {
1461
- if (payload.response === 'OK') {
1462
- throw new Error('listener bug');
1463
- }
1464
- });
1465
-
1466
- await client.noop();
1467
- let mailbox = await client.mailboxOpen('INBOX');
1468
- test.ok(mailbox, 'commands succeed despite the throwing listener');
1469
-
1470
- await client.logout();
1471
- client.close();
1472
- server.close();
1473
- test.done();
1474
- };