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,431 +0,0 @@
1
- 'use strict';
2
-
3
- // Live integration tests against a real IMAP4rev2 server (Dovecot 2.4+ in Docker).
4
- // Started via `npm run test:rev2` (see run-rev2-tests.sh) - excluded from the
5
- // Gruntfile nodeunit config so `npm test` stays Docker-free.
6
- //
7
- // The container uses a static passdb: any username logs in with the password
8
- // "pass", and every user gets a fresh mail home, so each test connects with a
9
- // unique user for isolation.
10
-
11
- const { ImapFlow } = require('../../lib/imap-flow');
12
-
13
- const HOST = process.env.IMAPFLOW_TEST_HOST || '127.0.0.1';
14
- const PORT = Number(process.env.IMAPFLOW_TEST_PORT) || 31143;
15
-
16
- let userCounter = 0;
17
-
18
- const connectClient = async (options, logs) => {
19
- let client = new ImapFlow(
20
- Object.assign(
21
- {
22
- host: HOST,
23
- port: PORT,
24
- secure: false,
25
- // The test container upgrades via STARTTLS with a self-signed cert
26
- tls: { rejectUnauthorized: false },
27
- auth: { user: `livetest-${Date.now()}-${++userCounter}`, pass: 'pass' },
28
- logger: false,
29
- emitLogs: !!logs
30
- },
31
- options || {}
32
- )
33
- );
34
- if (logs) {
35
- client.on('log', entry => logs.push(entry));
36
- }
37
- await client.connect();
38
- return client;
39
- };
40
-
41
- const wireLines = logs => logs.filter(entry => entry && typeof entry.msg === 'string' && ['c', 's'].includes(entry.src));
42
- const clientSent = (logs, needle) => wireLines(logs).some(entry => entry.src === 'c' && entry.msg.includes(needle));
43
- // Matches only an untagged response for the given command ("* ESEARCH ...").
44
- // A plain substring check would false-positive on capability listings - the
45
- // login response advertises tokens like ESEARCH in its CAPABILITY list.
46
- const serverSentUntagged = (logs, command) => {
47
- let re = new RegExp(`^\\* ${command}( |$)`);
48
- return wireLines(logs).some(entry => entry.src === 's' && re.test(entry.msg));
49
- };
50
-
51
- module.exports['Live rev2: connect negotiates ENABLE IMAP4rev2'] = async test => {
52
- const client = await connectClient();
53
- try {
54
- test.ok(client.capabilities.has('IMAP4rev2'), 'server should advertise IMAP4rev2');
55
- test.ok(client.enabled.has('IMAP4REV2'), 'IMAP4rev2 should be enabled');
56
- // The single merged ENABLE call must not lose the other extensions
57
- test.ok(client.enabled.has('CONDSTORE'), 'CONDSTORE should still be enabled');
58
- } finally {
59
- await client.logout();
60
- }
61
- test.done();
62
- };
63
-
64
- module.exports['Live rev2: list uses RETURN (SUBSCRIBED) and skips LSUB'] = async test => {
65
- const logs = [];
66
- const client = await connectClient(null, logs);
67
- try {
68
- const folders = await client.list();
69
-
70
- test.ok(clientSent(logs, 'SUBSCRIBED'), 'LIST should request RETURN (SUBSCRIBED)');
71
- test.ok(!clientSent(logs, 'LSUB'), 'LSUB must not be issued on a rev2 session');
72
-
73
- const sent = folders.find(folder => folder.path === 'Sent');
74
- test.ok(sent, 'Sent mailbox should be listed');
75
- test.equal(sent.subscribed, true, 'Sent should be reported as subscribed');
76
- test.equal(sent.specialUse, '\\Sent', 'special-use flag should be honored');
77
- // \HasNoChildren must survive the extended LIST (RETURN (CHILDREN) requested)
78
- test.ok(sent.flags.has('\\HasNoChildren'), 'child info should be present');
79
- for (let folder of folders) {
80
- test.ok(!folder.flags.has('\\Subscribed'), '\\Subscribed must be folded into the subscribed property');
81
- }
82
-
83
- const inbox = folders.find(folder => folder.path === 'INBOX');
84
- test.ok(inbox, 'INBOX should be listed');
85
- test.equal(inbox.subscribed, true, 'INBOX is always reported as subscribed');
86
- } finally {
87
- await client.logout();
88
- }
89
- test.done();
90
- };
91
-
92
- module.exports['Live rev2: subscription state round-trips'] = async test => {
93
- const client = await connectClient();
94
- try {
95
- await client.mailboxCreate('RoundTrip');
96
- await client.mailboxSubscribe('RoundTrip');
97
- let folders = await client.list();
98
- test.equal(folders.find(folder => folder.path === 'RoundTrip').subscribed, true);
99
-
100
- await client.mailboxUnsubscribe('RoundTrip');
101
- folders = await client.list();
102
- test.ok(!folders.find(folder => folder.path === 'RoundTrip').subscribed);
103
- } finally {
104
- await client.logout();
105
- }
106
- test.done();
107
- };
108
-
109
- module.exports['Live rev2: deleted-but-subscribed folders stay out of listings'] = async test => {
110
- const client = await connectClient();
111
- try {
112
- await client.mailboxCreate('Ghost');
113
- await client.mailboxSubscribe('Ghost');
114
- await client.mailboxDelete('Ghost');
115
-
116
- const folders = await client.list();
117
- // The RETURN (SUBSCRIBED) option must not resurrect phantom subscriptions
118
- // the way a raw LSUB would
119
- test.ok(!folders.find(folder => folder.path === 'Ghost'), 'deleted folder must not be listed');
120
- } finally {
121
- await client.logout();
122
- }
123
- test.done();
124
- };
125
-
126
- module.exports['Live rev2: UTF-8 mailbox names round-trip'] = async test => {
127
- const client = await connectClient();
128
- try {
129
- const name = 'T\u00f5rva \u00f5un';
130
- await client.mailboxCreate(name);
131
- const folders = await client.list();
132
- const created = folders.find(folder => folder.path === name);
133
- test.ok(created, 'UTF-8 mailbox should be listed under its literal name');
134
-
135
- const mailbox = await client.mailboxOpen(name);
136
- test.equal(mailbox.path, name);
137
- await client.mailboxClose();
138
- await client.mailboxDelete(name);
139
- } finally {
140
- await client.logout();
141
- }
142
- test.done();
143
- };
144
-
145
- module.exports['Live rev2: statusQuery is answered inline via LIST-STATUS'] = async test => {
146
- const logs = [];
147
- const client = await connectClient(null, logs);
148
- try {
149
- await client.append('INBOX', Buffer.from('Subject: status probe\r\n\r\nstatus probe body\r\n'));
150
-
151
- const folders = await client.list({ statusQuery: { messages: true, unseen: true } });
152
-
153
- // Pin the inline LIST-STATUS behavior: the STATUS request must ride on the
154
- // LIST command itself, and no standalone STATUS command may be issued -
155
- // a plain substring check would also pass on the fallback path
156
- const listLine = wireLines(logs).find(entry => entry.src === 'c' && /^\S+ LIST /.test(entry.msg));
157
- test.ok(listLine && listLine.msg.includes('RETURN') && listLine.msg.includes('STATUS'), 'LIST should carry RETURN (STATUS ...)');
158
- test.ok(!wireLines(logs).some(entry => entry.src === 'c' && /^\S+ STATUS /.test(entry.msg)), 'no standalone STATUS command should be needed');
159
- const inbox = folders.find(folder => folder.path === 'INBOX');
160
- test.equal(inbox.status.messages, 1, 'inline STATUS should report the appended message');
161
- } finally {
162
- await client.logout();
163
- }
164
- test.done();
165
- };
166
-
167
- module.exports['Live rev2: plain search results are collected'] = async test => {
168
- const logs = [];
169
- const client = await connectClient(null, logs);
170
- try {
171
- await client.append('INBOX', Buffer.from('Subject: first\r\n\r\nfirst\r\n'));
172
- await client.append('INBOX', Buffer.from('Subject: second\r\n\r\nsecond\r\n'));
173
-
174
- await client.mailboxOpen('INBOX');
175
- const results = await client.search({ all: true });
176
-
177
- // RFC 9051 deprecates the untagged SEARCH response in favor of ESEARCH,
178
- // but Dovecot 2.4 still answers a plain SEARCH with the legacy form even
179
- // on an ENABLEd rev2 session (verified on the wire). The client accepts
180
- // both forms, so assert that the results arrived via one of them - the
181
- // ESEARCH-answered variant of a plain SEARCH is covered by the mock
182
- // suite, and a real ESEARCH response is exercised live in the
183
- // returnOptions test below.
184
- test.ok(serverSentUntagged(logs, 'SEARCH') || serverSentUntagged(logs, 'ESEARCH'), 'results should arrive via an untagged SEARCH or ESEARCH response');
185
- test.deepEqual(results, [1, 2]);
186
- } finally {
187
- await client.logout();
188
- }
189
- test.done();
190
- };
191
-
192
- module.exports['Live rev2: returnOptions search is answered via a real ESEARCH response'] = async test => {
193
- const logs = [];
194
- const client = await connectClient(null, logs);
195
- try {
196
- await client.append('INBOX', Buffer.from('Subject: first\r\n\r\nfirst\r\n'));
197
- await client.append('INBOX', Buffer.from('Subject: second\r\n\r\nsecond\r\n'));
198
-
199
- await client.mailboxOpen('INBOX');
200
- const result = await client.search({ all: true }, { returnOptions: ['ALL', 'COUNT'] });
201
-
202
- // SEARCH RETURN (...) makes Dovecot answer with a genuine untagged
203
- // ESEARCH response - this exercises the ESEARCH parsing path against a
204
- // real server
205
- test.ok(serverSentUntagged(logs, 'ESEARCH'), 'server should reply with an untagged ESEARCH response');
206
- test.ok(!serverSentUntagged(logs, 'SEARCH'), 'no legacy untagged SEARCH response is expected');
207
- test.equal(result.count, 2);
208
- test.equal(result.all, '1:2');
209
- } finally {
210
- await client.logout();
211
- }
212
- test.done();
213
- };
214
-
215
- module.exports['Live rev2: SEARCHRES saved result is usable as a sequence set'] = async test => {
216
- // SEARCH RETURN (SAVE) stores the result server-side (RFC 5182) and '$'
217
- // references it in later commands. '$' has to make it through the outgoing
218
- // sequence-set validation for that to work at all.
219
- const client = await connectClient();
220
- try {
221
- test.ok(client.capabilities.has('SEARCHRES'), 'Dovecot should advertise SEARCHRES');
222
-
223
- await client.append('INBOX', Buffer.from('Subject: first\r\n\r\nfirst\r\n'));
224
- await client.append('INBOX', Buffer.from('Subject: second\r\n\r\nsecond\r\n'));
225
-
226
- await client.mailboxOpen('INBOX');
227
- await client.search({ subject: 'second' }, { uid: true, returnOptions: ['SAVE'] });
228
-
229
- let flagResult = await client.messageFlagsAdd('$', ['\\Flagged'], { uid: true });
230
- test.ok(flagResult, 'STORE against the saved result must succeed');
231
-
232
- let messages = [];
233
- for await (let msg of client.fetch('$', { flags: true, envelope: true }, { uid: true })) {
234
- messages.push(msg);
235
- }
236
- test.equal(messages.length, 1, 'the saved result references exactly the matched message');
237
- test.equal(messages[0].envelope.subject, 'second');
238
- test.ok(messages[0].flags.has('\\Flagged'), 'the flag change applied to the saved result');
239
- } finally {
240
- await client.logout();
241
- }
242
- test.done();
243
- };
244
-
245
- module.exports['Live rev2: MODSEQ search criterion surfaces modseq from the ESEARCH response'] = async test => {
246
- const client = await connectClient();
247
- try {
248
- await client.append('INBOX', Buffer.from('Subject: first\r\n\r\nfirst\r\n'));
249
- await client.append('INBOX', Buffer.from('Subject: second\r\n\r\nsecond\r\n'));
250
-
251
- await client.mailboxOpen('INBOX');
252
- // RFC 7162: a MODSEQ criterion on a CONDSTORE session makes the server
253
- // append MODSEQ to the ESEARCH response
254
- const result = await client.search({ modseq: 1 }, { returnOptions: ['ALL', 'COUNT'] });
255
-
256
- test.equal(result.count, 2);
257
- test.equal(result.all, '1:2');
258
- test.ok(typeof result.modseq === 'bigint' && result.modseq > 0n, 'modseq should surface as a positive BigInt');
259
- } finally {
260
- await client.logout();
261
- }
262
- test.done();
263
- };
264
-
265
- module.exports['Live rev2: STATUS reports SIZE and DELETED'] = async test => {
266
- const logs = [];
267
- const client = await connectClient(null, logs);
268
- try {
269
- const raw = Buffer.from('Subject: sized\r\n\r\nsized body\r\n');
270
- await client.append('INBOX', raw, ['\\Deleted']);
271
- await client.append('INBOX', Buffer.from('Subject: kept\r\n\r\nkept body\r\n'));
272
-
273
- const status = await client.status('INBOX', { messages: true, size: true, deleted: true });
274
-
275
- test.ok(clientSent(logs, 'SIZE'), 'STATUS should request SIZE');
276
- test.ok(clientSent(logs, 'DELETED'), 'STATUS should request DELETED');
277
- test.equal(status.messages, 2);
278
- test.equal(status.deleted, 1, 'one message carries the \\Deleted flag');
279
- test.ok(Number.isSafeInteger(status.size) && status.size >= raw.length, 'mailbox size should cover at least the first appended message');
280
- } finally {
281
- await client.logout();
282
- }
283
- test.done();
284
- };
285
-
286
- module.exports['Live rev2: statusQuery returns SIZE and DELETED inline via LIST-STATUS'] = async test => {
287
- const client = await connectClient();
288
- try {
289
- await client.append('INBOX', Buffer.from('Subject: probe\r\n\r\nprobe body\r\n'), ['\\Deleted']);
290
-
291
- const folders = await client.list({ statusQuery: { messages: true, size: true, deleted: true } });
292
- const inbox = folders.find(folder => folder.path === 'INBOX');
293
- test.equal(inbox.status.messages, 1);
294
- test.equal(inbox.status.deleted, 1);
295
- test.ok(Number.isSafeInteger(inbox.status.size) && inbox.status.size > 0);
296
- } finally {
297
- await client.logout();
298
- }
299
- test.done();
300
- };
301
-
302
- module.exports['Live rev2: SELECT response carries an untagged LIST and re-select gets CLOSED'] = async test => {
303
- const logs = [];
304
- const client = await connectClient(null, logs);
305
- try {
306
- await client.mailboxCreate('Closer');
307
- const mailbox = await client.mailboxOpen('INBOX');
308
- test.equal(mailbox.path, 'INBOX');
309
-
310
- // RFC 9051 6.3.1: the SELECT response includes an untagged LIST for the
311
- // selected mailbox - the client must consume it without issue. Scoped to
312
- // the SELECT exchange itself: mailboxOpen() also issues its own LIST
313
- // command first, whose untagged replies would satisfy a global check
314
- // even if the SELECT response omitted the LIST
315
- const lines = wireLines(logs);
316
- const selectIdx = lines.findIndex(entry => entry.src === 'c' && /^\S+ SELECT /.test(entry.msg));
317
- test.ok(selectIdx >= 0, 'SELECT command should be on the wire');
318
- const selectTag = lines[selectIdx].msg.split(' ')[0];
319
- const doneIdx = lines.findIndex((entry, i) => i > selectIdx && entry.src === 's' && entry.msg.startsWith(`${selectTag} `));
320
- const listInSelect = lines.some((entry, i) => i > selectIdx && i < doneIdx && entry.src === 's' && /^\* LIST( |$)/.test(entry.msg));
321
- test.ok(listInSelect, 'rev2 SELECT response should include an untagged LIST response');
322
-
323
- // switching mailboxes must produce a CLOSED response code for the old one
324
- await client.mailboxOpen('Closer');
325
- const closed = wireLines(logs).some(entry => entry.src === 's' && entry.msg.includes('[CLOSED]'));
326
- test.ok(closed, 're-select should carry a CLOSED response code');
327
- test.equal(client.mailbox.path, 'Closer', 'client state should track the newly selected mailbox');
328
-
329
- await client.mailboxClose();
330
- await client.mailboxDelete('Closer');
331
- } finally {
332
- await client.logout();
333
- }
334
- test.done();
335
- };
336
-
337
- module.exports['Live rev2: binary fetch uses the folded-in FETCH BINARY'] = async test => {
338
- const logs = [];
339
- const client = await connectClient(null, logs);
340
- try {
341
- // BINARY sections only allow numeric part specifiers, so use a multipart
342
- // message - part "1" of a single-part message would resolve to the TEXT
343
- // section, which must stay a BODY fetch. The base64 encoding gives the
344
- // server-side BINARY decoding something to undo.
345
- const content = [
346
- 'Subject: bin',
347
- 'MIME-Version: 1.0',
348
- 'Content-Type: multipart/mixed; boundary=bb',
349
- '',
350
- '--bb',
351
- 'Content-Type: text/plain',
352
- 'Content-Transfer-Encoding: base64',
353
- '',
354
- Buffer.from('binary body').toString('base64'),
355
- '--bb--',
356
- ''
357
- ].join('\r\n');
358
- await client.append('INBOX', Buffer.from(content));
359
-
360
- await client.mailboxOpen('INBOX');
361
- const { content: downloadStream } = await client.download('1', '1', { binary: true });
362
- const chunks = [];
363
- for await (let chunk of downloadStream) {
364
- chunks.push(chunk);
365
- }
366
-
367
- test.ok(clientSent(logs, 'BINARY.PEEK[1]'), 'client should issue a BINARY fetch for the numeric part on a rev2 session');
368
- test.equal(Buffer.concat(chunks).toString().trim(), 'binary body', 'BINARY fetch should return the decoded content');
369
- } finally {
370
- await client.logout();
371
- }
372
- test.done();
373
- };
374
-
375
- module.exports['Live rev2: MOVE reports COPYUID from the untagged OK'] = async test => {
376
- const client = await connectClient();
377
- try {
378
- await client.append('INBOX', Buffer.from('Subject: mover\r\n\r\nmover body\r\n'));
379
- await client.mailboxCreate('Moved');
380
-
381
- await client.mailboxOpen('INBOX');
382
- const result = await client.messageMove('1', 'Moved');
383
-
384
- // RFC 9051 6.4.8: the server is REQUIRED to send COPYUID in an untagged OK
385
- // before the EXPUNGEs - verify the client captured it
386
- test.ok(result, 'move should succeed');
387
- test.equal(result.path, 'INBOX');
388
- test.equal(result.destination, 'Moved');
389
- test.ok(result.uidMap && result.uidMap.size === 1, 'COPYUID must be captured from the untagged OK');
390
-
391
- await client.mailboxClose();
392
- await client.mailboxDelete('Moved');
393
- } finally {
394
- await client.logout();
395
- }
396
- test.done();
397
- };
398
-
399
- module.exports['Live rev2: message lifecycle smoke test'] = async test => {
400
- const client = await connectClient();
401
- try {
402
- await client.append('INBOX', Buffer.from('Subject: smoke\r\n\r\nsmoke body\r\n'), ['\\Seen']);
403
-
404
- await client.mailboxOpen('INBOX');
405
- const message = await client.fetchOne('1', { envelope: true, flags: true, uid: true });
406
- test.equal(message.envelope.subject, 'smoke');
407
- test.ok(message.flags.has('\\Seen'));
408
-
409
- await client.mailboxCreate('Smoke');
410
- await client.messageCopy('1', 'Smoke');
411
- await client.messageMove('1', 'Smoke');
412
-
413
- const status = await client.status('Smoke', { messages: true });
414
- test.equal(status.messages, 2, 'copy plus move should land two messages');
415
-
416
- // uid expunge - UIDPLUS is folded into base rev2, so only the requested
417
- // message may disappear
418
- await client.mailboxOpen('Smoke');
419
- const first = await client.fetchOne('1', { uid: true });
420
- await client.messageDelete(`${first.uid}`, { uid: true });
421
- const statusAfter = await client.status('Smoke', { messages: true });
422
- test.equal(statusAfter.messages, 1, 'UID EXPUNGE must remove only the targeted message');
423
-
424
- await client.mailboxClose();
425
- await client.mailboxRename('Smoke', 'Smoke2');
426
- await client.mailboxDelete('Smoke2');
427
- } finally {
428
- await client.logout();
429
- }
430
- test.done();
431
- };
@@ -1,75 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- # Runs the ImapFlow live integration tests against a real IMAP4rev2 server
5
- # (Dovecot 2.4+ in Docker). Opt-in via `npm run test:rev2` - not part of the
6
- # regular `npm test` run, which stays Docker-free.
7
- #
8
- # Environment overrides:
9
- # IMAPFLOW_DOVECOT_IMAGE image to run (default dovecot/dovecot:2.4.4)
10
- # IMAPFLOW_DOVECOT_PLATFORM e.g. linux/amd64; defaults to the host platform.
11
- # Note: forcing linux/amd64 on Apple Silicon does
12
- # not work - Rosetta cannot start Dovecot's
13
- # privilege-separated login processes.
14
- # IMAPFLOW_TEST_PORT host port to publish (default 31143)
15
-
16
- CONTAINER_NAME="${IMAPFLOW_DOVECOT_CONTAINER:-imapflow-rev2-test}"
17
- IMAGE="${IMAPFLOW_DOVECOT_IMAGE:-dovecot/dovecot:2.4.4}"
18
- PORT="${IMAPFLOW_TEST_PORT:-31143}"
19
-
20
- # Empty unless a platform override was requested; --platform=value keeps it a
21
- # single argument so plain ${PLATFORM_ARG:+...} expansion works under set -u
22
- PLATFORM_ARG="${IMAPFLOW_DOVECOT_PLATFORM:+--platform=$IMAPFLOW_DOVECOT_PLATFORM}"
23
-
24
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
- PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
26
-
27
- cleanup() {
28
- docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
29
- }
30
- trap cleanup EXIT
31
- cleanup
32
-
33
- # Guard against a stale image cached for the wrong architecture: `docker run`
34
- # without --platform silently reuses a local image even when its architecture
35
- # does not match the host (e.g. an amd64 image left behind on an arm64 host),
36
- # and Dovecot then fails to start with confusing emulation errors. Only applies
37
- # when no explicit platform override was requested.
38
- if [ -z "${IMAPFLOW_DOVECOT_PLATFORM:-}" ] && docker image inspect "$IMAGE" >/dev/null 2>&1; then
39
- image_arch="$(docker image inspect --format '{{.Architecture}}' "$IMAGE" 2>/dev/null || true)"
40
- host_arch="$(docker version --format '{{.Server.Arch}}' 2>/dev/null || true)"
41
- if [ -n "$image_arch" ] && [ -n "$host_arch" ] && [ "$image_arch" != "$host_arch" ]; then
42
- echo "Local $IMAGE image is $image_arch but the Docker host is $host_arch - re-pulling for linux/$host_arch..."
43
- docker pull --platform "linux/$host_arch" "$IMAGE"
44
- fi
45
- fi
46
-
47
- docker run ${PLATFORM_ARG:+"$PLATFORM_ARG"} -d --name "$CONTAINER_NAME" \
48
- -e USER_PASSWORD=pass \
49
- -v "$SCRIPT_DIR/dovecot-test.conf:/etc/dovecot/conf.d/99-imapflow-test.conf:ro" \
50
- -p "127.0.0.1:$PORT:31143" \
51
- "$IMAGE" >/dev/null
52
-
53
- echo "Waiting for Dovecot to accept IMAP connections on port $PORT..."
54
- for i in $(seq 1 30); do
55
- if node -e "
56
- const net = require('net');
57
- const socket = net.connect(Number(process.argv[1]), '127.0.0.1');
58
- const bail = code => { socket.destroy(); process.exit(code); };
59
- socket.on('data', chunk => bail(chunk.toString().startsWith('* OK') ? 0 : 1));
60
- socket.on('error', () => bail(1));
61
- setTimeout(() => bail(1), 2000);
62
- " "$PORT" 2>/dev/null; then
63
- echo "Dovecot is ready"
64
- break
65
- fi
66
- if [ "$i" = 30 ]; then
67
- echo "Dovecot container did not become ready" >&2
68
- docker logs "$CONTAINER_NAME" >&2 || true
69
- exit 1
70
- fi
71
- sleep 1
72
- done
73
-
74
- cd "$PROJECT_DIR"
75
- IMAPFLOW_TEST_HOST=127.0.0.1 IMAPFLOW_TEST_PORT="$PORT" npx nodeunit test/integration/rev2-live-test.js
@@ -1,83 +0,0 @@
1
- 'use strict';
2
-
3
- const { ImapFlow } = require('../lib/imap-flow');
4
-
5
- module.exports['Integration: Basic client creation'] = test => {
6
- let client = new ImapFlow({
7
- host: 'imap.example.com',
8
- auth: { user: 'test', pass: 'test' }
9
- });
10
-
11
- test.ok(client);
12
- test.equal(client.state, client.states.NOT_AUTHENTICATED);
13
- test.equal(client.authenticated, false);
14
- test.done();
15
- };
16
-
17
- module.exports['Integration: Multiple client instances'] = test => {
18
- let client1 = new ImapFlow({
19
- host: 'imap1.example.com',
20
- auth: { user: 'test1', pass: 'test1' }
21
- });
22
-
23
- let client2 = new ImapFlow({
24
- host: 'imap2.example.com',
25
- auth: { user: 'test2', pass: 'test2' }
26
- });
27
-
28
- test.ok(client1);
29
- test.ok(client2);
30
- test.notEqual(client1.id, client2.id);
31
- test.done();
32
- };
33
-
34
- module.exports['Integration: Client configuration'] = test => {
35
- let client = new ImapFlow({
36
- host: 'imap.example.com',
37
- port: 993,
38
- secure: true,
39
- auth: { user: 'test', pass: 'test' },
40
- logger: false
41
- });
42
-
43
- test.equal(client.host, 'imap.example.com');
44
- test.equal(client.port, 993);
45
- test.equal(client.secureConnection, true);
46
- test.done();
47
- };
48
-
49
- module.exports['Integration: Event emitter functionality'] = test => {
50
- let client = new ImapFlow({
51
- host: 'imap.example.com',
52
- auth: { user: 'test', pass: 'test' }
53
- });
54
-
55
- let eventFired = false;
56
- client.on('test-event', () => {
57
- eventFired = true;
58
- });
59
-
60
- client.emit('test-event');
61
-
62
- setTimeout(() => {
63
- test.ok(eventFired);
64
- test.done();
65
- }, 10);
66
- };
67
-
68
- module.exports['Integration: Stats functionality'] = test => {
69
- let client = new ImapFlow({
70
- host: 'imap.example.com',
71
- auth: { user: 'test', pass: 'test' }
72
- });
73
-
74
- let stats = client.stats();
75
- test.ok(typeof stats === 'object');
76
- test.ok(Object.prototype.hasOwnProperty.call(stats, 'sent'));
77
- test.ok(Object.prototype.hasOwnProperty.call(stats, 'received'));
78
-
79
- // Reset stats
80
- let resetStats = client.stats(true);
81
- test.ok(typeof resetStats === 'object');
82
- test.done();
83
- };