imapflow 1.7.8 → 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 +20 -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 +761 -1789
  202. package/dist/esm/jp-decoder.d.ts +12 -0
  203. package/{lib → dist/esm}/jp-decoder.js +6 -21
  204. package/dist/esm/limited-passthrough.d.ts +25 -0
  205. package/{lib → dist/esm}/limited-passthrough.js +7 -20
  206. package/dist/esm/logger.d.ts +3 -0
  207. package/dist/esm/logger.js +4 -0
  208. package/dist/esm/package-info.d.ts +3 -0
  209. package/dist/esm/package-info.js +4 -0
  210. package/dist/esm/package.json +3 -0
  211. package/dist/esm/proxy-connection.d.ts +33 -0
  212. package/{lib → dist/esm}/proxy-connection.js +56 -127
  213. package/dist/esm/search-compiler.d.ts +34 -0
  214. package/{lib → dist/esm}/search-compiler.js +54 -110
  215. package/dist/esm/special-use.d.ts +22 -0
  216. package/dist/esm/special-use.js +907 -0
  217. package/dist/esm/tools.d.ts +427 -0
  218. package/dist/esm/tools.js +1446 -0
  219. package/dist/esm/types.d.ts +828 -0
  220. package/dist/esm/types.js +4 -0
  221. package/package.json +60 -20
  222. package/.gitattributes +0 -1
  223. package/.github/CODE_OF_CONDUCT.md +0 -76
  224. package/.github/FUNDING.yml +0 -4
  225. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  226. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  227. package/.github/contributing.md +0 -17
  228. package/.github/workflows/release.yaml +0 -36
  229. package/.github/workflows/stale.yml +0 -29
  230. package/.github/workflows/test.yml +0 -51
  231. package/.ncurc.js +0 -4
  232. package/.prettierignore +0 -4
  233. package/.prettierrc.js +0 -8
  234. package/.release-please-manifest.json +0 -3
  235. package/CLAUDE.md +0 -104
  236. package/Gruntfile.js +0 -23
  237. package/eslint.config.js +0 -45
  238. package/lib/handler/imap-formal-syntax.js +0 -189
  239. package/lib/handler/imap-handler.js +0 -17
  240. package/lib/imap-commands.js +0 -45
  241. package/lib/logger.js +0 -5
  242. package/lib/special-use.js +0 -923
  243. package/lib/tools.js +0 -1612
  244. package/release-please-config.json +0 -10
  245. package/test/authentication-test.js +0 -101
  246. package/test/auto-idle-test.js +0 -470
  247. package/test/bodystructure-test.js +0 -899
  248. package/test/charsets-test.js +0 -161
  249. package/test/commands-branches-test.js +0 -1095
  250. package/test/commands-integration-test.js +0 -11124
  251. package/test/commands-test.js +0 -73
  252. package/test/connection-edge-cases-test.js +0 -1828
  253. package/test/connection-test.js +0 -162
  254. package/test/copyuid-parser-test.js +0 -173
  255. package/test/fetch-generator-test.js +0 -218
  256. package/test/fixtures/fake-timers.js +0 -115
  257. package/test/fixtures/serialized-mimetorture.js +0 -2738
  258. package/test/fixtures/test-client.js +0 -101
  259. package/test/fixtures/test-tls.js +0 -8
  260. package/test/handler-branches-test.js +0 -310
  261. package/test/idle-polling-test.js +0 -518
  262. package/test/imap-compiler-test.js +0 -809
  263. package/test/imap-flow-compress-test.js +0 -166
  264. package/test/imap-flow-coverage-test.js +0 -612
  265. package/test/imap-flow-fetch-download-test.js +0 -909
  266. package/test/imap-flow-internals-test.js +0 -725
  267. package/test/imap-flow-methods-test.js +0 -889
  268. package/test/imap-flow-proxy-paths-test.js +0 -366
  269. package/test/imap-flow-secure-test.js +0 -573
  270. package/test/imap-flow-server-test.js +0 -1474
  271. package/test/imap-formal-syntax-test.js +0 -293
  272. package/test/imap-parser-test.js +0 -1474
  273. package/test/imap-stream-edge-cases-test.js +0 -666
  274. package/test/imap-stream-test.js +0 -177
  275. package/test/imapflow-test.js +0 -258
  276. package/test/integration/README.md +0 -52
  277. package/test/integration/dovecot-test.conf +0 -27
  278. package/test/integration/rev2-live-test.js +0 -431
  279. package/test/integration/run-rev2-tests.sh +0 -75
  280. package/test/integration-test.js +0 -83
  281. package/test/jp-decoder-test.js +0 -304
  282. package/test/limited-passthrough-test.js +0 -299
  283. package/test/memory-cleanup-test.js +0 -144
  284. package/test/memory-leak-test.js +0 -667
  285. package/test/parser-limits-test.js +0 -292
  286. package/test/proxy-connection-test.js +0 -738
  287. package/test/reliability-improvements-test.js +0 -548
  288. package/test/search-compiler-test.js +0 -1300
  289. package/test/search-test.js +0 -329
  290. package/test/special-use-test.js +0 -418
  291. package/test/starttls-injection-test.js +0 -181
  292. package/test/tag-correlation-test.js +0 -333
  293. package/test/timer-policy-test.js +0 -227
  294. package/test/token-parser-test.js +0 -456
  295. package/test/tools-test.js +0 -2013
  296. package/test/unhandled-rejection-test.js +0 -661
@@ -1,573 +0,0 @@
1
- 'use strict';
2
-
3
- // End-to-end secure-transport tests: a successful STARTTLS upgrade and a direct
4
- // TLS connection, both against in-process mock servers using a self-signed cert.
5
- // These cover upgradeToSTARTTLS()'s success path and the TLS branches of connect().
6
-
7
- const net = require('net');
8
- const tls = require('tls');
9
- const { ImapFlow } = require('../lib/imap-flow');
10
- const { cert, key } = require('./fixtures/test-tls');
11
-
12
- const CAPS = 'IMAP4rev1 ID ENABLE NAMESPACE';
13
-
14
- // Shared per-connection IMAP line handler used by both the plaintext and the
15
- // upgraded TLS phases. Returns responses for the minimal session commands.
16
- // `caps` is the full capability list to advertise for this phase - the STARTTLS
17
- // test uses different pre- and post-TLS sets to prove the client discards the
18
- // plaintext capabilities and re-fetches them over TLS (RFC 9051 6.2.1).
19
- const handleLine = (sock, line, onStartTls, caps) => {
20
- caps = caps || `${CAPS} STARTTLS`;
21
- let parts = line.split(' ');
22
- let tag = parts[0];
23
- let cmd = (parts[1] || '').toUpperCase();
24
- switch (cmd) {
25
- case 'CAPABILITY':
26
- sock.write(`* CAPABILITY ${caps}\r\n${tag} OK CAPABILITY done\r\n`);
27
- break;
28
- case 'STARTTLS':
29
- sock.write(`${tag} OK Begin TLS\r\n`);
30
- if (onStartTls) {
31
- onStartTls();
32
- }
33
- break;
34
- case 'ID':
35
- sock.write(`* ID ("name" "mock" "version" "1")\r\n${tag} OK ID done\r\n`);
36
- break;
37
- case 'LOGIN':
38
- sock.write(`${tag} OK LOGIN done\r\n`);
39
- break;
40
- case 'NAMESPACE':
41
- sock.write(`* NAMESPACE (("" "/")) NIL NIL\r\n${tag} OK NAMESPACE done\r\n`);
42
- break;
43
- case 'ENABLE':
44
- sock.write(`${tag} OK ENABLE done\r\n`);
45
- break;
46
- case 'COMPRESS':
47
- sock.write(`${tag} NO not now\r\n`);
48
- break;
49
- case 'NOOP':
50
- sock.write(`${tag} OK NOOP done\r\n`);
51
- break;
52
- case 'LOGOUT':
53
- sock.write(`* BYE bye\r\n${tag} OK LOGOUT done\r\n`);
54
- break;
55
- default:
56
- sock.write(`${tag} BAD unknown ${cmd}\r\n`);
57
- }
58
- };
59
-
60
- const lineReader = (sock, onLine) => {
61
- let buf = '';
62
- let handler = data => {
63
- buf += data.toString('binary');
64
- let idx;
65
- while ((idx = buf.indexOf('\r\n')) >= 0) {
66
- let line = buf.slice(0, idx);
67
- buf = buf.slice(idx + 2);
68
- onLine(line);
69
- }
70
- };
71
- sock.on('data', handler);
72
- sock.on('error', () => {});
73
- return () => sock.removeListener('data', handler);
74
- };
75
-
76
- const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve(server.address().port)));
77
-
78
- // Builds a STARTTLS mock server. Defaults give the happy path; options tweak a
79
- // phase without another copy of the upgrade scaffold:
80
- // - preTlsCaps / postTlsCaps: capability list advertised before / after the upgrade
81
- // - startTlsOk: tag => string, overrides the response line to the STARTTLS command
82
- // - onTlsLine: (tlsSocket, line) => boolean, intercepts post-upgrade lines; return
83
- // true when the line was handled
84
- const createStartTlsServer = (opts = {}) =>
85
- net.createServer(rawSocket => {
86
- rawSocket.on('error', () => {});
87
-
88
- let preTlsCaps = opts.preTlsCaps || `${CAPS} STARTTLS`;
89
- let postTlsCaps = opts.postTlsCaps || CAPS;
90
-
91
- let detachPlain;
92
- detachPlain = lineReader(rawSocket, line => {
93
- let parts = line.split(' ');
94
- let tag = parts[0];
95
- let cmd = (parts[1] || '').toUpperCase();
96
-
97
- if (cmd === 'STARTTLS') {
98
- rawSocket.write(opts.startTlsOk ? opts.startTlsOk(tag) : `${tag} OK Begin TLS\r\n`);
99
- detachPlain();
100
- let tlsSocket = new tls.TLSSocket(rawSocket, { isServer: true, key, cert });
101
- tlsSocket.on('error', () => {});
102
- lineReader(tlsSocket, l => {
103
- if (opts.onTlsLine && opts.onTlsLine(tlsSocket, l)) {
104
- return;
105
- }
106
- handleLine(tlsSocket, l, null, postTlsCaps);
107
- });
108
- return;
109
- }
110
-
111
- handleLine(rawSocket, line, null, preTlsCaps);
112
- });
113
-
114
- rawSocket.write(`* OK [CAPABILITY ${preTlsCaps}] ready\r\n`);
115
- });
116
-
117
- // The client options shared by every STARTTLS test.
118
- const makeStartTlsClient = (port, overrides = {}) =>
119
- new ImapFlow({
120
- host: '127.0.0.1',
121
- port,
122
- secure: false,
123
- doSTARTTLS: true,
124
- servername: 'localhost',
125
- tls: { rejectUnauthorized: false },
126
- disableAutoIdle: true,
127
- disableCompression: true,
128
- logger: false,
129
- auth: { user: 'test', pass: 'secret' },
130
- ...overrides
131
- });
132
-
133
- // Every terminal upgrade path must leave no upgrade state behind
134
- const assertUpgradeSettled = (test, client) => {
135
- test.equal(client.upgrading, false, 'upgrading flag cleared');
136
- test.equal(client._upgradeReject, null, 'upgrade rejector cleared');
137
- test.equal(client.upgradeTimeout, null, 'upgrade timer cleared');
138
- };
139
-
140
- // ---------------------------------------------------------------------------
141
- // STARTTLS happy path
142
- // ---------------------------------------------------------------------------
143
-
144
- module.exports['Secure: STARTTLS upgrade completes a session'] = async test => {
145
- // post-TLS phase advertises a different capability set
146
- let server = createStartTlsServer({
147
- preTlsCaps: `${CAPS} STARTTLS PRETLS-ONLY`,
148
- postTlsCaps: `${CAPS} POSTTLS-ONLY`
149
- });
150
-
151
- let port = await listen(server);
152
- let client = makeStartTlsClient(port);
153
- client.on('error', () => {});
154
-
155
- await client.connect();
156
- test.ok(client.secureConnection, 'connection upgraded to TLS');
157
- test.ok(client.authenticated, 'authenticated over TLS');
158
- test.ok(client.usable);
159
- // RFC 9051 6.2.1: capabilities cached before STARTTLS MUST be discarded and
160
- // re-fetched over the TLS channel
161
- test.ok(client.capabilities.has('POSTTLS-ONLY'), 'post-TLS capabilities were re-fetched');
162
- test.ok(!client.capabilities.has('PRETLS-ONLY'), 'pre-TLS capabilities were discarded');
163
-
164
- await client.noop();
165
- await client.logout();
166
- client.close();
167
- server.close();
168
- test.done();
169
- };
170
-
171
- module.exports['Secure: STARTTLS discards capabilities even when the OK carries a CAPABILITY code'] = async test => {
172
- // A server (or a MITM rewriting the plaintext stream) may stamp [CAPABILITY ...]
173
- // on the STARTTLS OK itself. That marks the capability set as freshly updated, so
174
- // a discard conditioned on "an update is still pending" would keep exactly the
175
- // pre-TLS list an attacker controls - the list that then chooses the AUTH
176
- // mechanism and answers LOGINDISABLED. RFC 9051 6.2.1 makes the discard mandatory.
177
- let server = createStartTlsServer({
178
- preTlsCaps: `${CAPS} STARTTLS PRETLS-ONLY`,
179
- postTlsCaps: `${CAPS} POSTTLS-ONLY`,
180
- // the OK itself carries a (pre-TLS, attacker-rewritable) CAPABILITY code
181
- startTlsOk: tag => `${tag} OK [CAPABILITY ${CAPS} PRETLS-ONLY] Begin TLS\r\n`
182
- });
183
-
184
- let port = await listen(server);
185
- let client = makeStartTlsClient(port);
186
- client.on('error', () => {});
187
-
188
- await client.connect();
189
- test.ok(client.secureConnection, 'connection upgraded to TLS');
190
- test.ok(client.capabilities.has('POSTTLS-ONLY'), 'post-TLS capabilities were re-fetched');
191
- test.ok(!client.capabilities.has('PRETLS-ONLY'), 'pre-TLS capabilities were discarded');
192
-
193
- await client.logout();
194
- client.close();
195
- server.close();
196
- test.done();
197
- };
198
-
199
- module.exports['Secure: pre-TLS rawCapabilities do not survive a failed re-fetch'] = async test => {
200
- // capabilities/authCapabilities are discarded at the upgrade, but rawCapabilities
201
- // is public surface external consumers read. If the post-TLS CAPABILITY re-fetch
202
- // fails, the pre-TLS list - the one an active attacker can rewrite - must not
203
- // linger there either.
204
- let server = createStartTlsServer({
205
- preTlsCaps: `${CAPS} STARTTLS PRETLS-ONLY`,
206
- postTlsCaps: `${CAPS} POSTTLS-ONLY`,
207
- onTlsLine: (tlsSocket, line) => {
208
- let parts = line.split(' ');
209
- if ((parts[1] || '').toUpperCase() === 'CAPABILITY') {
210
- // the re-fetch over TLS fails
211
- tlsSocket.write(`${parts[0]} NO CAPABILITY not available\r\n`);
212
- return true;
213
- }
214
- return false;
215
- }
216
- });
217
-
218
- let port = await listen(server);
219
- let client = makeStartTlsClient(port);
220
- client.on('error', () => {});
221
-
222
- await client.connect();
223
- test.ok(client.secureConnection, 'connection upgraded to TLS');
224
- test.ok(!client.capabilities.has('PRETLS-ONLY'), 'pre-TLS capabilities were discarded');
225
- let raw = [].concat(client.rawCapabilities || []);
226
- test.ok(!raw.some(entry => entry && /PRETLS-ONLY/.test(entry.value || entry)), 'pre-TLS rawCapabilities were discarded');
227
-
228
- await client.logout();
229
- client.close();
230
- server.close();
231
- test.done();
232
- };
233
-
234
- // Records every socket that went through configureSocket(), which is the single place the
235
- // transport options (keepalive + inactivity watchdog) are applied.
236
- const trackConfiguredSockets = client => {
237
- let sockets = [];
238
- let original = client.configureSocket.bind(client);
239
- client.configureSocket = socket => {
240
- sockets.push(socket);
241
- return original(socket);
242
- };
243
- return sockets;
244
- };
245
-
246
- module.exports['Secure: STARTTLS session keeps the inactivity watchdog'] = async test => {
247
- // Regression: the inactivity timer was armed on the plain socket only, while the timeout
248
- // listener was attached to the TLS socket, so an upgraded session had no watchdog at all
249
- // and a dead connection was never noticed.
250
- let server = createStartTlsServer();
251
- let port = await listen(server);
252
- let client = makeStartTlsClient(port, { socketTimeout: 200 });
253
-
254
- let errors = [];
255
- client.on('error', err => errors.push(err));
256
- let closed = new Promise(resolve => client.once('close', resolve));
257
- let configured = trackConfiguredSockets(client);
258
-
259
- await client.connect();
260
- test.ok(client.secureConnection, 'connection upgraded to TLS');
261
- test.equal(configured.length, 2, 'both the plain socket and the upgraded socket are configured');
262
- test.equal(client.socket.timeout, 200, 'the upgraded socket carries the configured inactivity timeout');
263
- test.equal(configured[0].timeout, 0, 'the superseded plain-socket timer is cleared, not left armed');
264
-
265
- // No traffic from here on: the watchdog has to fire and take the connection down.
266
- await closed;
267
-
268
- test.ok(
269
- errors.some(err => err.code === 'ETIMEOUT'),
270
- 'the inactivity watchdog reported a socket timeout'
271
- );
272
- test.ok(client.isClosed, 'the connection closed after the timeout');
273
-
274
- client.close();
275
- server.close();
276
- test.done();
277
- };
278
-
279
- module.exports['Secure: STARTTLS leaves no upgrade state behind on success'] = async test => {
280
- // Item 8: every terminal path of the upgrade runs through one settlement helper, so after
281
- // a successful handshake no timer, rejector, flag or temporary handler survives.
282
- let server = createStartTlsServer();
283
- let port = await listen(server);
284
- let client = makeStartTlsClient(port);
285
- client.on('error', () => {});
286
-
287
- await client.connect();
288
-
289
- assertUpgradeSettled(test, client);
290
- // The handshake-only handler is gone, leaving the generic socket error handler as the single
291
- // error path (during the handshake it is the other way round, which is what prevents a
292
- // handshake error from firing two handlers at once).
293
- test.equal(client.socket.listenerCount('error'), 1, 'the TLS socket keeps exactly one error path');
294
-
295
- await client.logout();
296
- client.close();
297
- server.close();
298
- test.done();
299
- };
300
-
301
- module.exports['Secure: close() during a STARTTLS upgrade settles the upgrade'] = async test => {
302
- // The server acknowledges STARTTLS but never performs a handshake. close() has to settle
303
- // the pending upgrade instead of leaving the session promise dangling forever.
304
- let server = net.createServer(rawSocket => {
305
- rawSocket.on('error', () => {});
306
- let detach = lineReader(rawSocket, line => {
307
- handleLine(rawSocket, line, () => {
308
- detach(); // silence: no ServerHello, no data at all
309
- });
310
- });
311
- rawSocket.write(`* OK [CAPABILITY ${CAPS} STARTTLS] ready\r\n`);
312
- });
313
- let port = await listen(server);
314
- let client = makeStartTlsClient(port);
315
- client.on('error', () => {});
316
-
317
- let connectResult = client.connect().then(
318
- () => null,
319
- err => err
320
- );
321
-
322
- // Wait until the upgrade is in flight, then close the connection under it.
323
- while (!client.upgrading) {
324
- await new Promise(resolve => setTimeout(resolve, 10));
325
- }
326
- // A late socket event would invoke the same settle helper the rejector exposes -
327
- // capture it before close() consumes it
328
- let settle = client._upgradeReject;
329
- client.close();
330
-
331
- let err = await connectResult;
332
- test.ok(err, 'connect rejected rather than hanging on the abandoned upgrade');
333
- test.ok(['ClosedAfterConnectText', 'ClosedAfterConnectTLS', 'NoConnection'].includes(err.code), `connect rejected with ${err.code}`);
334
- assertUpgradeSettled(test, client);
335
-
336
- // The late event lands after the upgrade already settled - it must be a no-op:
337
- // a settled upgrade neither claims the error as a TLS failure nor re-arms any state
338
- let lateErr = new Error('late socket error');
339
- settle(lateErr);
340
- test.ok(!lateErr.tlsFailed, 'the late error was not marked as a TLS upgrade failure');
341
- assertUpgradeSettled(test, client);
342
-
343
- server.close();
344
- test.done();
345
- };
346
-
347
- module.exports['Secure: STARTTLS handshake failure rejects connect'] = async test => {
348
- let server = createStartTlsServer();
349
- let port = await listen(server);
350
- // The tls option is erased entirely, not merely relaxed: certificate validation
351
- // stays on, so the mock server's self-signed certificate must fail the handshake,
352
- // and the upgrade builds its TLS options from the no-options fallback
353
- let client = makeStartTlsClient(port, { tls: undefined });
354
- client.on('error', () => {});
355
-
356
- let err = await client.connect().then(
357
- () => null,
358
- connectErr => connectErr
359
- );
360
-
361
- test.ok(err, 'connect rejected on the failed handshake');
362
- test.ok(err.tlsFailed, 'the error is marked as a TLS upgrade failure');
363
- assertUpgradeSettled(test, client);
364
-
365
- client.close();
366
- server.close();
367
- test.done();
368
- };
369
-
370
- // ---------------------------------------------------------------------------
371
- // Direct TLS connection
372
- // ---------------------------------------------------------------------------
373
-
374
- module.exports['Secure: direct TLS connection completes a session'] = async test => {
375
- let server = tls.createServer({ key, cert }, sock => {
376
- sock.on('error', () => {});
377
- lineReader(sock, line => handleLine(sock, line));
378
- sock.write(`* OK [CAPABILITY ${CAPS}] ready\r\n`);
379
- });
380
-
381
- let port = await listen(server);
382
- let client = new ImapFlow({
383
- host: '127.0.0.1',
384
- port,
385
- secure: true,
386
- servername: 'localhost',
387
- tls: { rejectUnauthorized: false },
388
- disableAutoIdle: true,
389
- disableCompression: true,
390
- logger: false,
391
- auth: { user: 'test', pass: 'secret' }
392
- });
393
- client.on('error', () => {});
394
-
395
- await client.connect();
396
- test.ok(client.secureConnection);
397
- test.ok(client.authenticated);
398
- test.ok(client.tls, 'cipher info recorded');
399
-
400
- await client.noop();
401
- await client.logout();
402
- client.close();
403
- server.close();
404
- test.done();
405
- };
406
-
407
- // ---------------------------------------------------------------------------
408
- // STARTTLS required but unsupported -> fail closed
409
- // ---------------------------------------------------------------------------
410
-
411
- module.exports['Secure: doSTARTTLS=false skips an advertised upgrade'] = async test => {
412
- let server = net.createServer(sock => {
413
- sock.on('error', () => {});
414
- lineReader(sock, line => handleLine(sock, line)); // advertises STARTTLS in CAPABILITY
415
- sock.write(`* OK [CAPABILITY ${CAPS} STARTTLS] ready\r\n`);
416
- });
417
- let port = await listen(server);
418
- let client = new ImapFlow({
419
- host: '127.0.0.1',
420
- port,
421
- secure: false,
422
- doSTARTTLS: false, // explicitly disabled -> stay plaintext
423
- tls: { rejectUnauthorized: false },
424
- disableAutoIdle: true,
425
- disableCompression: true,
426
- logger: false,
427
- auth: { user: 'test', pass: 'secret' }
428
- });
429
- client.on('error', () => {});
430
-
431
- await client.connect();
432
- test.ok(!client.secureConnection, 'stayed on plaintext');
433
- test.ok(client.authenticated);
434
-
435
- await client.logout();
436
- client.close();
437
- server.close();
438
- test.done();
439
- };
440
-
441
- module.exports['Secure: STARTTLS command rejected by server fails closed'] = async test => {
442
- let server = net.createServer(sock => {
443
- sock.on('error', () => {});
444
- lineReader(sock, line => {
445
- let parts = line.split(' ');
446
- let tag = parts[0];
447
- let cmd = (parts[1] || '').toUpperCase();
448
- if (cmd === 'CAPABILITY') {
449
- sock.write(`* CAPABILITY ${CAPS} STARTTLS\r\n${tag} OK done\r\n`);
450
- } else if (cmd === 'STARTTLS') {
451
- sock.write(`${tag} NO STARTTLS not available right now\r\n`);
452
- } else {
453
- sock.write(`${tag} OK ok\r\n`);
454
- }
455
- });
456
- sock.write(`* OK [CAPABILITY ${CAPS} STARTTLS] ready\r\n`);
457
- });
458
- let port = await listen(server);
459
- let client = new ImapFlow({
460
- host: '127.0.0.1',
461
- port,
462
- secure: false,
463
- doSTARTTLS: true,
464
- tls: { rejectUnauthorized: false },
465
- disableAutoIdle: true,
466
- disableCompression: true,
467
- logger: false,
468
- auth: { user: 'test', pass: 'secret' }
469
- });
470
- client.on('error', () => {});
471
-
472
- let err = null;
473
- try {
474
- await client.connect();
475
- } catch (e) {
476
- err = e;
477
- }
478
- test.ok(err, 'connect rejected when STARTTLS refused');
479
- test.ok(err.tlsFailed);
480
-
481
- client.close();
482
- server.close();
483
- test.done();
484
- };
485
-
486
- module.exports['Secure: STARTTLS handshake error rejects connect'] = async test => {
487
- // Server acknowledges STARTTLS then sends non-TLS garbage so the client's TLS
488
- // handshake fails, exercising the dedicated TLS-socket error handler.
489
- let server = net.createServer(rawSocket => {
490
- rawSocket.on('error', () => {});
491
- let detach = lineReader(rawSocket, line => {
492
- handleLine(rawSocket, line, () => {
493
- detach();
494
- // garbage instead of a TLS ServerHello
495
- rawSocket.write(Buffer.from('this is definitely not a TLS handshake\r\n'.repeat(8)));
496
- });
497
- });
498
- rawSocket.write(`* OK [CAPABILITY ${CAPS} STARTTLS] ready\r\n`);
499
- });
500
- let port = await listen(server);
501
- let client = new ImapFlow({
502
- host: '127.0.0.1',
503
- port,
504
- secure: false,
505
- doSTARTTLS: true,
506
- servername: 'localhost',
507
- tls: { rejectUnauthorized: false },
508
- disableAutoIdle: true,
509
- disableCompression: true,
510
- logger: false,
511
- auth: { user: 'test', pass: 'secret' }
512
- });
513
- client.on('error', () => {});
514
-
515
- let err = null;
516
- try {
517
- await client.connect();
518
- } catch (e) {
519
- err = e;
520
- }
521
- test.ok(err, 'connect rejected on TLS handshake failure');
522
- test.ok(err.tlsFailed, 'flagged as a TLS failure');
523
-
524
- client.close();
525
- server.close();
526
- test.done();
527
- };
528
-
529
- module.exports['Secure: STARTTLS required but not advertised throws'] = async test => {
530
- let server = net.createServer(sock => {
531
- sock.on('error', () => {});
532
- lineReader(sock, line => {
533
- let parts = line.split(' ');
534
- let tag = parts[0];
535
- let cmd = (parts[1] || '').toUpperCase();
536
- if (cmd === 'CAPABILITY') {
537
- // no STARTTLS advertised
538
- sock.write(`* CAPABILITY ${CAPS}\r\n${tag} OK done\r\n`);
539
- } else {
540
- sock.write(`${tag} OK ok\r\n`);
541
- }
542
- });
543
- sock.write(`* OK [CAPABILITY ${CAPS}] ready\r\n`);
544
- });
545
-
546
- let port = await listen(server);
547
- let client = new ImapFlow({
548
- host: '127.0.0.1',
549
- port,
550
- secure: false,
551
- doSTARTTLS: true,
552
- servername: 'localhost',
553
- tls: { rejectUnauthorized: false },
554
- disableAutoIdle: true,
555
- disableCompression: true,
556
- logger: false,
557
- auth: { user: 'test', pass: 'secret' }
558
- });
559
- client.on('error', () => {});
560
-
561
- let err = null;
562
- try {
563
- await client.connect();
564
- } catch (e) {
565
- err = e;
566
- }
567
- test.ok(err, 'connect rejected');
568
- test.ok(err.tlsFailed, 'flagged as TLS failure');
569
-
570
- client.close();
571
- server.close();
572
- test.done();
573
- };