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,873 +0,0 @@
1
- 'use strict';
2
-
3
- // Tests for the fetch generator and the download / downloadMany streaming
4
- // orchestration in ImapFlow. These exercise the real method bodies while
5
- // stubbing the lower-level run()/fetchOne() so message data is fully controlled.
6
-
7
- const { ImapFlow } = require('../lib/imap-flow');
8
- const libbase64 = require('libbase64');
9
- const libqp = require('libqp');
10
- const libmime = require('libmime');
11
- const { Writable, finished } = require('stream');
12
- const { chunkedFetchOne } = require('./fixtures/test-client');
13
-
14
- const makeClient = (overrides = {}) => {
15
- let client = new ImapFlow({
16
- host: 'imap.example.com',
17
- port: 993,
18
- auth: { user: 'test', pass: 'test' },
19
- logger: false,
20
- ...overrides
21
- });
22
- client.socket = { destroyed: false, destroy: () => {} };
23
- client.usable = true;
24
- client.mailbox = { path: 'INBOX', exists: 10 };
25
- client.state = client.states.SELECTED;
26
- return client;
27
- };
28
-
29
- const collect = stream =>
30
- new Promise((resolve, reject) => {
31
- let chunks = [];
32
- stream.on('data', c => chunks.push(c));
33
- stream.on('end', () => resolve(Buffer.concat(chunks)));
34
- stream.on('error', reject);
35
- });
36
-
37
- // ============================================================================
38
- // fetch() generator
39
- // ============================================================================
40
-
41
- module.exports['Fetch: generator returns nothing without mailbox'] = async test => {
42
- let client = makeClient();
43
- client.mailbox = false;
44
- let results = [];
45
- for await (let msg of client.fetch('1:*', { uid: true })) {
46
- results.push(msg);
47
- }
48
- test.equal(results.length, 0);
49
- test.done();
50
- };
51
-
52
- module.exports['Fetch: generator returns when range resolves empty'] = async test => {
53
- let client = makeClient();
54
- let results = [];
55
- for await (let msg of client.fetch([], { uid: true })) {
56
- results.push(msg);
57
- }
58
- test.equal(results.length, 0);
59
- test.done();
60
- };
61
-
62
- module.exports['Fetch: generator yields untagged fetch responses'] = async test => {
63
- let client = makeClient();
64
- client.run = async (command, range, query, options) => {
65
- test.equal(command, 'FETCH');
66
- // Simulate the FETCH handler pushing two rows
67
- options.onUntaggedFetch({ seq: 1, uid: 11 }, () => {});
68
- options.onUntaggedFetch({ seq: 2, uid: 12 }, () => {});
69
- return true;
70
- };
71
- let results = [];
72
- for await (let msg of client.fetch('1:2', { uid: true })) {
73
- results.push(msg);
74
- }
75
- test.equal(results.length, 2);
76
- test.equal(results[0].uid, 11);
77
- test.equal(results[1].uid, 12);
78
- test.done();
79
- };
80
-
81
- module.exports['Fetch: generator breaks early and drains backpressure'] = async test => {
82
- let client = makeClient();
83
- let nextCalls = 0;
84
- client.run = async (command, range, query, options) => {
85
- options.onUntaggedFetch({ seq: 1, uid: 11 }, () => nextCalls++);
86
- options.onUntaggedFetch({ seq: 2, uid: 12 }, () => nextCalls++);
87
- options.onUntaggedFetch({ seq: 3, uid: 13 }, () => nextCalls++);
88
- return true;
89
- };
90
- let results = [];
91
- for await (let msg of client.fetch('1:3', {})) {
92
- results.push(msg);
93
- break; // exit early -> finally{} must drain remaining next() callbacks
94
- }
95
- test.equal(results.length, 1);
96
- // The yielded item's next plus the queued items get drained
97
- test.ok(nextCalls >= 1);
98
- test.done();
99
- };
100
-
101
- module.exports['Fetch: generator throws when run rejects'] = async test => {
102
- let client = makeClient();
103
- client.run = async () => {
104
- throw new Error('FETCH failed');
105
- };
106
- let threw = null;
107
- try {
108
- // eslint-disable-next-line no-unused-vars
109
- for await (let msg of client.fetch('1:2', {})) {
110
- // no-op
111
- }
112
- } catch (err) {
113
- threw = err;
114
- }
115
- test.ok(threw);
116
- test.equal(threw.message, 'FETCH failed');
117
- test.done();
118
- };
119
-
120
- module.exports['Fetch: generator throws if connection closes mid-iteration'] = async test => {
121
- let client = makeClient();
122
- client.run = async (command, range, query, options) => {
123
- // queue two rows so a second loop iteration runs after we close the socket
124
- options.onUntaggedFetch({ seq: 1, uid: 11 }, () => {});
125
- options.onUntaggedFetch({ seq: 2, uid: 12 }, () => {});
126
- return true;
127
- };
128
- let threw = null;
129
- try {
130
- // eslint-disable-next-line no-unused-vars
131
- for await (let msg of client.fetch('1:2', {})) {
132
- // close the connection before the next iteration's guard runs
133
- client.socket.destroyed = true;
134
- }
135
- } catch (err) {
136
- threw = err;
137
- }
138
- test.ok(threw);
139
- test.equal(threw.code, 'EConnectionClosed');
140
- test.done();
141
- };
142
-
143
- // ============================================================================
144
- // fetchOne() / fetchAll()
145
- // ============================================================================
146
-
147
- module.exports['Fetch: fetchOne returns undefined without mailbox'] = async test => {
148
- let client = makeClient();
149
- client.mailbox = false;
150
- let res = await client.fetchOne('1', {});
151
- test.equal(res, undefined);
152
- test.done();
153
- };
154
-
155
- module.exports['Fetch: fetchOne star returns false on empty mailbox'] = async test => {
156
- let client = makeClient();
157
- client.mailbox.exists = 0;
158
- let res = await client.fetchOne('*', {});
159
- test.equal(res, false);
160
- test.done();
161
- };
162
-
163
- module.exports['Fetch: fetchOne star uses last message seq'] = async test => {
164
- let client = makeClient();
165
- client.mailbox.exists = 7;
166
- let captured = null;
167
- client.run = async (command, seq, query, options) => {
168
- captured = { seq, options };
169
- return { list: [{ seq: 7, uid: 70 }] };
170
- };
171
- let res = await client.fetchOne('*', {});
172
- test.equal(captured.seq, '7');
173
- test.equal(captured.options.uid, false);
174
- test.equal(res.uid, 70);
175
- test.done();
176
- };
177
-
178
- module.exports['Fetch: fetchOne returns false when list empty'] = async test => {
179
- let client = makeClient();
180
- client.run = async () => ({ list: [] });
181
- let res = await client.fetchOne('1', {});
182
- test.equal(res, false);
183
- test.done();
184
- };
185
-
186
- module.exports['Fetch: fetchOne returns first message'] = async test => {
187
- let client = makeClient();
188
- client.run = async () => ({ list: [{ seq: 1, uid: 11 }] });
189
- let res = await client.fetchOne('1', { uid: true });
190
- test.equal(res.uid, 11);
191
- test.done();
192
- };
193
-
194
- module.exports['Fetch: fetchAll collects all generator results'] = async test => {
195
- let client = makeClient();
196
- client.run = async (command, range, query, options) => {
197
- options.onUntaggedFetch({ seq: 1, uid: 11 }, () => {});
198
- options.onUntaggedFetch({ seq: 2, uid: 12 }, () => {});
199
- return true;
200
- };
201
- let all = await client.fetchAll('1:2', {});
202
- test.equal(all.length, 2);
203
- test.done();
204
- };
205
-
206
- // ============================================================================
207
- // download()
208
- // ============================================================================
209
-
210
- module.exports['Download: returns empty object without mailbox'] = async test => {
211
- let client = makeClient();
212
- client.mailbox = false;
213
- let res = await client.download('1');
214
- test.deepEqual(res, {});
215
- test.done();
216
- };
217
-
218
- module.exports['Download: full message in multiple chunks'] = async test => {
219
- let client = makeClient();
220
- let body = Buffer.from('A'.repeat(10));
221
- client.fetchOne = chunkedFetchOne(body);
222
- let { meta, content } = await client.download('1', false, { chunkSize: 4 });
223
- test.equal(meta.contentType, 'message/rfc822');
224
- test.equal(meta.expectedSize, 10);
225
- let data = await collect(content);
226
- test.equal(data.toString(), 'A'.repeat(10));
227
- test.done();
228
- };
229
-
230
- module.exports['Download: falls back to default chunkSize/maxBytes when zero'] = async test => {
231
- let client = makeClient();
232
- let body = Buffer.from('tiny');
233
- client.fetchOne = chunkedFetchOne(body);
234
- // zero values are falsy -> the (|| default) fallbacks kick in
235
- let { content } = await client.download('1', false, { chunkSize: 0, maxBytes: 0 });
236
- let data = await collect(content);
237
- test.equal(data.toString(), 'tiny');
238
- test.done();
239
- };
240
-
241
- module.exports['Download: flowed pipeline forwards a mid-stream fetch error'] = async test => {
242
- let client = makeClient();
243
- let mime = Buffer.from('Content-Type: text/plain; format=flowed\r\nContent-Disposition: inline\r\n\r\n');
244
- let calls = 0;
245
- client.fetchOne = async () => {
246
- calls++;
247
- if (calls === 1) {
248
- let bodyParts = new Map();
249
- bodyParts.set('2.mime', mime);
250
- bodyParts.set('2', Buffer.alloc(64, 0x61));
251
- return { uid: 1, size: 1000, bodyParts };
252
- }
253
- throw new Error('mid-stream flowed fetch fail');
254
- };
255
- let { content } = await client.download('1', '2', { chunkSize: 64 });
256
- let streamErr;
257
- content.on('data', () => {});
258
- // stream.finished() fires once with the error (if any) when the stream is
259
- // truly done — no end-vs-error resolve race.
260
- await new Promise(resolve =>
261
- finished(content, err => {
262
- streamErr = err;
263
- resolve();
264
- })
265
- );
266
- test.ok(streamErr, 'error forwarded through the flowed decoder pipeline');
267
- test.done();
268
- };
269
-
270
- module.exports['Download: returns {} when fetchOne yields no chunk'] = async test => {
271
- let client = makeClient();
272
- client.fetchOne = async () => false;
273
- let res = await client.download('1', false, { chunkSize: 4 });
274
- test.deepEqual(res, {});
275
- test.done();
276
- };
277
-
278
- module.exports['Download: base64 body part is decoded'] = async test => {
279
- let client = makeClient();
280
- let mime = Buffer.from('Content-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\n');
281
- let encoded = Buffer.from(libbase64.encode(Buffer.from('Hello World')));
282
- client.fetchOne = async () => {
283
- let bodyParts = new Map();
284
- bodyParts.set('2.mime', mime);
285
- bodyParts.set('2', encoded);
286
- return { uid: 1, size: encoded.length, bodyParts };
287
- };
288
- let { meta, content } = await client.download('1', '2', { chunkSize: 1024 });
289
- test.equal(meta.contentType, 'text/plain');
290
- test.equal(meta.encoding, 'base64');
291
- let data = await collect(content);
292
- test.equal(data.toString(), 'Hello World');
293
- test.done();
294
- };
295
-
296
- module.exports['Download: binary-fetched part skips the client-side transfer decoding'] = async test => {
297
- let client = makeClient();
298
- // BINARY fetch (RFC 3516, FETCH side folded into IMAP4rev2): the server
299
- // decodes the content-transfer-encoding itself, so the client gets raw bytes
300
- // even though the MIME headers still declare base64 - decoding again would
301
- // corrupt the data. formatMessageResponse records the parts that arrived via
302
- // BINARY[...] in the binaryParts set, which the mock mirrors here.
303
- let mime = Buffer.from('Content-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\n');
304
- let decoded = Buffer.from('Hello World');
305
- client.fetchOne = async () => {
306
- let bodyParts = new Map();
307
- bodyParts.set('2.mime', mime);
308
- bodyParts.set('2', decoded);
309
- return { uid: 1, size: decoded.length, bodyParts, binaryParts: new Set(['2']) };
310
- };
311
- let { meta, content } = await client.download('1', '2', { chunkSize: 1024, binary: true });
312
- test.equal(meta.encoding, 'base64');
313
- let data = await collect(content);
314
- test.equal(data.toString(), 'Hello World');
315
- test.done();
316
- };
317
-
318
- module.exports['DownloadMany: binary-fetched parts skip the client-side transfer decoding'] = async test => {
319
- let client = makeClient();
320
- let mime = Buffer.from('Content-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\n');
321
- client.fetchOne = async () => {
322
- let bodyParts = new Map();
323
- bodyParts.set('2.mime', mime);
324
- bodyParts.set('2', Buffer.from('part two'));
325
- return { uid: 1, bodyParts, binaryParts: new Set(['2']) };
326
- };
327
- let res = await client.downloadMany('1', ['2'], { binary: true });
328
- test.equal(res['2'].content.toString(), 'part two');
329
- test.equal(res['2'].meta.encoding, 'base64');
330
- test.done();
331
- };
332
-
333
- module.exports['Download: BODY-answered part is still decoded even when binary was requested'] = async test => {
334
- let client = makeClient();
335
- // RFC 3516 lets a server answer a BINARY request with a NO or a BODY response
336
- // (e.g. [UNKNOWN-CTE]) - decoding must follow what actually arrived, so a part
337
- // absent from binaryParts gets the client-side decoder despite options.binary
338
- let mime = Buffer.from('Content-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\n');
339
- let encoded = Buffer.from(libbase64.encode(Buffer.from('Hello World')));
340
- client.fetchOne = async () => {
341
- let bodyParts = new Map();
342
- bodyParts.set('2.mime', mime);
343
- bodyParts.set('2', encoded);
344
- return { uid: 1, size: encoded.length, bodyParts };
345
- };
346
- let { content } = await client.download('1', '2', { chunkSize: 1024, binary: true });
347
- let data = await collect(content);
348
- test.equal(data.toString(), 'Hello World');
349
- test.done();
350
- };
351
-
352
- module.exports['Download: quoted-printable inline text with charset + filename'] = async test => {
353
- let client = makeClient();
354
- let mime = Buffer.from(
355
- 'Content-Type: text/plain; charset=iso-8859-1; name="f.txt"\r\n' +
356
- 'Content-Transfer-Encoding: quoted-printable\r\n' +
357
- 'Content-Disposition: inline; filename="f.txt"\r\n\r\n'
358
- );
359
- let encoded = Buffer.from(libqp.encode(Buffer.from('café', 'latin1')));
360
- client.fetchOne = async () => {
361
- let bodyParts = new Map();
362
- bodyParts.set('3.mime', mime);
363
- bodyParts.set('3', encoded);
364
- return { uid: 1, size: encoded.length, bodyParts };
365
- };
366
- let { meta, content } = await client.download('1', '3', { chunkSize: 1024 });
367
- test.equal(meta.encoding, 'quoted-printable');
368
- test.equal(meta.filename, 'f.txt');
369
- // charset is converted to utf-8 for text parts
370
- test.equal(meta.charset, 'utf-8');
371
- let data = await collect(content);
372
- test.equal(data.toString(), 'café');
373
- test.done();
374
- };
375
-
376
- module.exports['Download: part 1 single-node message becomes TEXT'] = async test => {
377
- let client = makeClient();
378
- let calls = [];
379
- let textBody = Buffer.from('plain text body');
380
- client.fetchOne = async (range, query) => {
381
- calls.push(query);
382
- if (query.bodyStructure) {
383
- // single node, no childNodes
384
- return { uid: 1, size: textBody.length, bodyStructure: { type: 'text/plain' } };
385
- }
386
- // part has been rewritten to 'text'
387
- let bodyParts = new Map();
388
- bodyParts.set('header', Buffer.from('Content-Type: text/plain\r\n\r\n'));
389
- bodyParts.set('text', textBody);
390
- return { uid: 1, size: textBody.length, bodyParts };
391
- };
392
- let { meta, content } = await client.download('1', '1', { chunkSize: 1024 });
393
- test.ok(meta);
394
- let data = await collect(content);
395
- test.equal(data.toString(), 'plain text body');
396
- test.done();
397
- };
398
-
399
- module.exports['Download: part 1 returns sentinel when bodyStructure fetch fails'] = async test => {
400
- let client = makeClient();
401
- client.fetchOne = async () => false;
402
- let res = await client.download('1', '1', { chunkSize: 1024 });
403
- test.deepEqual(res, { response: false, chunk: false });
404
- test.done();
405
- };
406
-
407
- module.exports['Download: maxBytes limits output'] = async test => {
408
- let client = makeClient();
409
- let body = Buffer.from('B'.repeat(20));
410
- client.fetchOne = async (range, query) => {
411
- let start = query.source.start;
412
- let maxLength = query.source.maxLength;
413
- return { uid: 1, size: body.length, source: body.slice(start, start + maxLength) };
414
- };
415
- let { content } = await client.download('1', false, { chunkSize: 4, maxBytes: 6 });
416
- let data = await collect(content);
417
- test.ok(data.length <= 6);
418
- test.done();
419
- };
420
-
421
- module.exports['Download: part 1 with no detectable content-type treated as text'] = async test => {
422
- let client = makeClient();
423
- let textBody = Buffer.from('untyped body text');
424
- client.fetchOne = async (range, query) => {
425
- if (query.bodyStructure) {
426
- return { uid: 1, size: textBody.length, bodyStructure: { type: 'text/plain' } }; // no childNodes
427
- }
428
- let bodyParts = new Map();
429
- // empty mime header -> no contentType detected -> isTextNode via (part==='1' && !contentType)
430
- bodyParts.set('header', Buffer.from('\r\n'));
431
- bodyParts.set('text', textBody);
432
- return { uid: 1, size: textBody.length, bodyParts };
433
- };
434
- let { content } = await client.download('1', '1', { chunkSize: 1024 });
435
- let data = await collect(content);
436
- test.equal(data.toString(), 'untyped body text');
437
- test.done();
438
- };
439
-
440
- module.exports['Download: returns {} when requested part missing from response'] = async test => {
441
- let client = makeClient();
442
- client.fetchOne = async () => {
443
- // response has a mime header but not the actual part content
444
- let bodyParts = new Map();
445
- bodyParts.set('5.mime', Buffer.from('Content-Type: text/plain\r\n\r\n'));
446
- return { uid: 1, size: 0, bodyParts };
447
- };
448
- let res = await client.download('1', '5', { chunkSize: 1024 });
449
- test.deepEqual(res, {});
450
- test.done();
451
- };
452
-
453
- module.exports['Download: format=flowed inline text is decoded'] = async test => {
454
- let client = makeClient();
455
- let mime = Buffer.from('Content-Type: text/plain; format=flowed; delsp=yes\r\nContent-Disposition: inline\r\n\r\n');
456
- let body = Buffer.from('soft \r\nbroken\r\n');
457
- client.fetchOne = async () => {
458
- let bodyParts = new Map();
459
- bodyParts.set('2.mime', mime);
460
- bodyParts.set('2', body);
461
- return { uid: 1, size: body.length, bodyParts };
462
- };
463
- let { meta, content } = await client.download('1', '2', { chunkSize: 1024 });
464
- test.ok(meta.flowed, 'flowed flag detected');
465
- let data = await collect(content);
466
- test.ok(data.length >= 0);
467
- test.done();
468
- };
469
-
470
- module.exports['Download: unknown charset is left undecoded'] = async test => {
471
- let client = makeClient();
472
- let mime = Buffer.from('Content-Type: text/plain; charset=totally-bogus-xyz\r\nContent-Disposition: inline\r\n\r\n');
473
- let body = Buffer.from('hello bogus charset');
474
- client.fetchOne = async () => {
475
- let bodyParts = new Map();
476
- bodyParts.set('2.mime', mime);
477
- bodyParts.set('2', body);
478
- return { uid: 1, size: body.length, bodyParts };
479
- };
480
- let { meta, content } = await client.download('1', '2', { chunkSize: 1024 });
481
- // getDecoder throws for the bogus charset; charset stays as the original
482
- test.equal(meta.charset, 'totally-bogus-xyz');
483
- let data = await collect(content);
484
- test.equal(data.toString(), 'hello bogus charset');
485
- test.done();
486
- };
487
-
488
- module.exports['Download: stops when a later chunk has no data'] = async test => {
489
- let client = makeClient();
490
- let calls = 0;
491
- client.fetchOne = async () => {
492
- calls++;
493
- if (calls <= 3) {
494
- // full-size chunks keep hasMore true
495
- return { uid: 1, size: 100, source: Buffer.from('ABCD') };
496
- }
497
- // subsequent fetch returns no source -> getNextPart returns {} -> loop breaks
498
- return { uid: 1, size: 100, source: null };
499
- };
500
- let { content } = await client.download('1', false, { chunkSize: 4 });
501
- let data = await collect(content);
502
- test.equal(data.toString(), 'ABCDABCDABCD');
503
- test.done();
504
- };
505
-
506
- module.exports['Download: initial chunk waits for drain when buffer full'] = async test => {
507
- let client = makeClient();
508
- let big = Buffer.alloc(64 * 1024, 0x61); // 64KB > default 16KB highWaterMark
509
- let calls = 0;
510
- client.fetchOne = async () => {
511
- calls++;
512
- if (calls <= 2) {
513
- return { uid: 1, size: 200 * 1024, source: big };
514
- }
515
- return { uid: 1, size: 200 * 1024, source: big.slice(0, 10) };
516
- };
517
- let { content } = await client.download('1', false, { chunkSize: 64 * 1024 });
518
-
519
- // Do NOT consume yet: with no consumer the first 64KB write returns false,
520
- // so download registers a 'drain' listener and waits before fetching more.
521
- await new Promise(resolve => setTimeout(resolve, 25));
522
-
523
- // Now drain it.
524
- let received = 0;
525
- content.on('data', c => {
526
- received += c.length;
527
- });
528
- await new Promise(resolve => content.on('end', resolve));
529
- test.ok(received > 64 * 1024);
530
- test.done();
531
- };
532
-
533
- module.exports['Download: in-loop backpressure waits for drain'] = async test => {
534
- let client = makeClient();
535
- let big = Buffer.alloc(64 * 1024, 0x61);
536
- let calls = 0;
537
- client.fetchOne = async () => {
538
- calls++;
539
- if (calls <= 4) {
540
- return { uid: 1, size: 400 * 1024, source: big };
541
- }
542
- return { uid: 1, size: 400 * 1024, source: big.slice(0, 10) };
543
- };
544
- let { content } = await client.download('1', false, { chunkSize: 64 * 1024 });
545
-
546
- // A slow consumer attached immediately. The first write succeeds (data moves
547
- // one buffer down) but subsequent loop writes return false while the consumer
548
- // is still draining the previous chunk, exercising the in-loop drain wait.
549
- let received = 0;
550
- let slow = new Writable({
551
- highWaterMark: 1,
552
- write(chunk, enc, cb) {
553
- received += chunk.length;
554
- setTimeout(cb, 40);
555
- }
556
- });
557
- content.pipe(slow);
558
- await new Promise(resolve => slow.on('finish', resolve));
559
-
560
- test.ok(received > 64 * 1024);
561
- test.done();
562
- };
563
-
564
- module.exports['Download: error during streaming surfaces on content stream'] = async test => {
565
- let client = makeClient();
566
- let calls = 0;
567
- client.fetchOne = async () => {
568
- calls++;
569
- if (calls === 1) {
570
- // metadata chunk (full size keeps hasMore true)
571
- return { uid: 1, size: 100, source: Buffer.alloc(4, 0x61) };
572
- }
573
- // subsequent loop fetch fails -> fetchAllParts rejects -> error on stream
574
- throw new Error('mid-stream fetch failure');
575
- };
576
- let { content } = await client.download('1', false, { chunkSize: 4 });
577
- let streamErr;
578
- content.on('data', () => {});
579
- await new Promise(resolve =>
580
- finished(content, err => {
581
- streamErr = err;
582
- resolve();
583
- })
584
- );
585
- test.ok(streamErr, 'error surfaced on content stream');
586
- test.equal(streamErr.message, 'mid-stream fetch failure');
587
- test.done();
588
- };
589
-
590
- module.exports['Download: write error on initial chunk surfaces on stream'] = async test => {
591
- let client = makeClient();
592
- // a non-Buffer source makes stream.write throw inside the setImmediate try/catch
593
- client.fetchOne = async () => ({ uid: 1, size: 100, source: 999999 });
594
- let { content } = await client.download('1', false, { chunkSize: 4 });
595
- let streamErr;
596
- content.on('data', () => {});
597
- await new Promise(resolve =>
598
- finished(content, err => {
599
- streamErr = err;
600
- resolve();
601
- })
602
- );
603
- test.ok(streamErr, 'write error surfaced');
604
- test.done();
605
- };
606
-
607
- module.exports['Download: destroying content before streaming aborts cleanly'] = async test => {
608
- let client = makeClient();
609
- let big = Buffer.alloc(64 * 1024, 0x61);
610
- client.fetchOne = async (range, query) => {
611
- let start = query.source.start;
612
- return { uid: 1, size: 500 * 1024, source: big.slice(start, start + 64 * 1024) };
613
- };
614
- let { content } = await client.download('1', false, { chunkSize: 64 * 1024 });
615
- // Destroy immediately: the deferred writeChunk sees a destroyed stream and bails.
616
- content.destroy();
617
- await new Promise(resolve => setTimeout(resolve, 30));
618
- test.ok(true, 'no crash when content destroyed before streaming starts');
619
- test.done();
620
- };
621
-
622
- module.exports['Download: aborting mid-backpressure stops the fetch loop'] = async test => {
623
- let client = makeClient();
624
- let big = Buffer.alloc(64 * 1024, 0x61);
625
- let calls = 0;
626
- client.fetchOne = async () => {
627
- calls++;
628
- if (calls <= 6) {
629
- return { uid: 1, size: 1000 * 1024, source: big };
630
- }
631
- return { uid: 1, size: 1000 * 1024, source: big.slice(0, 10) };
632
- };
633
- let { content } = await client.download('1', false, { chunkSize: 64 * 1024 });
634
-
635
- // Slow consumer to force backpressure, then destroy mid-stream so the loop
636
- // aborts during a drain wait.
637
- let slow = new Writable({
638
- highWaterMark: 1,
639
- write(chunk, enc, cb) {
640
- setTimeout(cb, 50);
641
- }
642
- });
643
- content.pipe(slow);
644
- setTimeout(() => content.destroy(), 70);
645
- await new Promise(resolve => {
646
- slow.on('finish', resolve);
647
- content.on('close', resolve);
648
- content.on('error', resolve);
649
- });
650
- test.ok(true, 'aborted mid-backpressure without hanging');
651
- test.done();
652
- };
653
-
654
- // ============================================================================
655
- // downloadMany()
656
- // ============================================================================
657
-
658
- module.exports['DownloadMany: returns empty object without mailbox'] = async test => {
659
- let client = makeClient();
660
- client.mailbox = false;
661
- let res = await client.downloadMany('1', ['2']);
662
- test.deepEqual(res, {});
663
- test.done();
664
- };
665
-
666
- module.exports['DownloadMany: returns {response:false} when no bodyParts'] = async test => {
667
- let client = makeClient();
668
- client.fetchOne = async () => ({ uid: 1 });
669
- let res = await client.downloadMany('1', ['2']);
670
- test.deepEqual(res, { response: false });
671
- test.done();
672
- };
673
-
674
- module.exports['DownloadMany: parses charset, flowed and name params'] = async test => {
675
- let client = makeClient();
676
- let mime = Buffer.from('Content-Type: text/plain; charset=iso-8859-1; format=flowed; delsp=yes; name="x.txt"\r\nContent-Disposition: inline\r\n\r\n');
677
- client.fetchOne = async () => {
678
- let bodyParts = new Map();
679
- bodyParts.set('2.mime', mime);
680
- bodyParts.set('2', Buffer.from('plain'));
681
- return { uid: 1, bodyParts };
682
- };
683
- let res = await client.downloadMany('1', ['2']);
684
- test.equal(res['2'].meta.charset, 'iso-8859-1');
685
- test.equal(res['2'].meta.flowed, true);
686
- test.equal(res['2'].meta.delSp, true);
687
- test.equal(res['2'].meta.filename, 'x.txt');
688
- test.done();
689
- };
690
-
691
- module.exports['DownloadMany: tolerates decodeWords failures for disposition/filename'] = async test => {
692
- let originalDecodeWords = libmime.decodeWords;
693
- libmime.decodeWords = () => {
694
- throw new Error('decodeWords boom');
695
- };
696
- try {
697
- let client = makeClient();
698
- let mime = Buffer.from('Content-Type: text/plain\r\nContent-Disposition: attachment; filename="f.txt"\r\n\r\n');
699
- client.fetchOne = async () => {
700
- let bodyParts = new Map();
701
- bodyParts.set('2.mime', mime);
702
- bodyParts.set('2', Buffer.from('x'));
703
- return { uid: 1, bodyParts };
704
- };
705
- let res = await client.downloadMany('1', ['2']);
706
- // disposition/filename are kept as-is when decodeWords throws
707
- test.equal(res['2'].meta.disposition, 'attachment');
708
- test.equal(res['2'].meta.filename, 'f.txt');
709
- } finally {
710
- libmime.decodeWords = originalDecodeWords;
711
- }
712
- test.done();
713
- };
714
-
715
- module.exports['Download: tolerates decodeWords failures for disposition/filename'] = async test => {
716
- let originalDecodeWords = libmime.decodeWords;
717
- libmime.decodeWords = () => {
718
- throw new Error('decodeWords boom');
719
- };
720
- try {
721
- let client = makeClient();
722
- let mime = Buffer.from('Content-Type: text/plain\r\nContent-Disposition: attachment; filename="f.txt"\r\n\r\n');
723
- client.fetchOne = async () => {
724
- let bodyParts = new Map();
725
- bodyParts.set('2.mime', mime);
726
- bodyParts.set('2', Buffer.from('hello'));
727
- return { uid: 1, size: 5, bodyParts };
728
- };
729
- let { meta, content } = await client.download('1', '2', { chunkSize: 1024 });
730
- test.equal(meta.disposition, 'attachment');
731
- test.equal(meta.filename, 'f.txt');
732
- await collect(content);
733
- } finally {
734
- libmime.decodeWords = originalDecodeWords;
735
- }
736
- test.done();
737
- };
738
-
739
- module.exports['DownloadMany: encoded parts with no content decode to null'] = async test => {
740
- let client = makeClient();
741
- let mime2 = Buffer.from('Content-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\n');
742
- let mime3 = Buffer.from('Content-Type: text/plain\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n');
743
- client.fetchOne = async () => {
744
- let bodyParts = new Map();
745
- // only the mime headers, no actual content parts -> content stays undefined
746
- bodyParts.set('2.mime', mime2);
747
- bodyParts.set('3.mime', mime3);
748
- return { uid: 1, bodyParts };
749
- };
750
- let res = await client.downloadMany('1', ['2', '3']);
751
- test.equal(res['2'].content, null); // base64 decode of missing content -> null
752
- test.equal(res['3'].content, null); // quoted-printable decode of missing content -> null
753
- test.done();
754
- };
755
-
756
- module.exports['DownloadMany: handles content part arriving before its mime header'] = async test => {
757
- let client = makeClient();
758
- client.fetchOne = async () => {
759
- let bodyParts = new Map();
760
- bodyParts.set('2', Buffer.from('hello')); // content first
761
- bodyParts.set('2.mime', Buffer.from('Content-Type: text/plain\r\n\r\n'));
762
- return { uid: 1, bodyParts };
763
- };
764
- let res = await client.downloadMany('1', ['2']);
765
- test.equal(res['2'].content.toString(), 'hello');
766
- test.equal(res['2'].meta.contentType, 'text/plain');
767
- test.done();
768
- };
769
-
770
- module.exports['Download: charset pipeline forwards a mid-stream fetch error'] = async test => {
771
- let client = makeClient();
772
- let mime = Buffer.from('Content-Type: text/plain; charset=iso-8859-1\r\nContent-Disposition: inline\r\n\r\n');
773
- let calls = 0;
774
- client.fetchOne = async () => {
775
- calls++;
776
- if (calls === 1) {
777
- let bodyParts = new Map();
778
- bodyParts.set('2.mime', mime);
779
- bodyParts.set('2', Buffer.alloc(64, 0x61)); // full chunk -> hasMore
780
- return { uid: 1, size: 1000, bodyParts };
781
- }
782
- throw new Error('mid-stream charset fetch fail');
783
- };
784
- let { content } = await client.download('1', '2', { chunkSize: 64 });
785
- let streamErr;
786
- content.on('data', () => {});
787
- await new Promise(resolve =>
788
- finished(content, err => {
789
- streamErr = err;
790
- resolve();
791
- })
792
- );
793
- test.ok(streamErr, 'error forwarded through the charset decoder pipeline');
794
- test.done();
795
- };
796
-
797
- // Covers the `(part === '1' && !meta.contentType)` text-node branch: for a
798
- // MULTIPART message, part '1' is NOT rewritten to TEXT, so requesting part '1'
799
- // whose MIME headers carry no Content-Type reaches that sub-condition.
800
- module.exports['Download: multipart part 1 without content-type streams as a text node'] = async test => {
801
- let client = makeClient();
802
- client.fetchOne = async (range, query) => {
803
- if (query.bodyStructure) {
804
- // childNodes present -> multipart -> part '1' stays '1'
805
- return {
806
- uid: 1,
807
- size: 11,
808
- bodyStructure: { type: 'multipart/mixed', childNodes: [{ type: 'text/plain' }, { type: 'application/octet-stream' }] }
809
- };
810
- }
811
- let bodyParts = new Map();
812
- // MIME headers with no Content-Type -> meta.contentType stays unset
813
- bodyParts.set('1.mime', Buffer.from('\r\n'));
814
- bodyParts.set('1', Buffer.from('hello world'));
815
- return { uid: 1, size: 11, bodyParts };
816
- };
817
- let { content } = await client.download('1', '1', { chunkSize: 64 });
818
- let data = await collect(content);
819
- test.equal(data.toString(), 'hello world');
820
- test.done();
821
- };
822
-
823
- module.exports['DownloadMany: decodes base64 and quoted-printable parts'] = async test => {
824
- let client = makeClient();
825
- let mime2 = Buffer.from('Content-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\n');
826
- let mime3 = Buffer.from(
827
- 'Content-Type: application/octet-stream; name="a.bin"\r\n' +
828
- 'Content-Transfer-Encoding: quoted-printable\r\n' +
829
- 'Content-Disposition: attachment; filename="a.bin"\r\n\r\n'
830
- );
831
- client.fetchOne = async (range, query) => {
832
- test.ok(query.bodyParts.includes('2.mime'));
833
- let bodyParts = new Map();
834
- bodyParts.set('2.mime', mime2);
835
- bodyParts.set('2', Buffer.from(libbase64.encode(Buffer.from('part two'))));
836
- bodyParts.set('3.mime', mime3);
837
- bodyParts.set('3', Buffer.from(libqp.encode(Buffer.from('part three'))));
838
- return { uid: 1, bodyParts };
839
- };
840
- let res = await client.downloadMany('1', ['2', '3']);
841
- test.equal(res['2'].content.toString(), 'part two');
842
- test.equal(res['2'].meta.contentType, 'text/plain');
843
- test.equal(res['3'].content.toString(), 'part three');
844
- test.equal(res['3'].meta.filename, 'a.bin');
845
- test.equal(res['3'].meta.disposition, 'attachment');
846
- test.done();
847
- };
848
-
849
- module.exports['Download: a buffering transform cannot defeat maxBytes'] = async test => {
850
- // format=flowed unwrapping and the Japanese charset decoder both hold their whole input
851
- // before emitting anything, so the limiter at the tail of the pipeline reports
852
- // `limited === false` however much the server sends. Without a bound of their own the fetch
853
- // loop would keep pulling the entire part.
854
- for (let contentType of ['text/plain; format=flowed', 'text/plain; charset=iso-2022-jp']) {
855
- let client = makeClient();
856
- let mime = Buffer.from(`Content-Type: ${contentType}\r\nContent-Disposition: inline\r\n\r\n`);
857
- let calls = 0;
858
- client.fetchOne = async () => {
859
- calls++;
860
- let bodyParts = new Map();
861
- bodyParts.set('2.mime', mime);
862
- // full-size chunks keep hasMore true; the loop must stop on its own, not run dry
863
- bodyParts.set('2', calls <= 40 ? Buffer.alloc(1024, 0x61) : Buffer.alloc(1));
864
- return { uid: 1, size: 1024 * 1024, bodyParts };
865
- };
866
-
867
- let { content } = await client.download('1', '2', { chunkSize: 1024, maxBytes: 512 });
868
- let data = await collect(content);
869
- test.ok(data.length <= 512, `${contentType}: delivered ${data.length} bytes, expected no more than 512`);
870
- test.ok(calls <= 3, `${contentType}: the fetch loop must stop at the bound, made ${calls} calls`);
871
- }
872
- test.done();
873
- };