imapflow 1.7.8 → 2.0.1

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 +520 -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 +3956 -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} +387 -517
  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 +60 -123
  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 +769 -1790
  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,2013 +0,0 @@
1
- 'use strict';
2
-
3
- const tools = require('../lib/tools');
4
- const { parser } = require('../lib/handler/imap-handler');
5
- const crypto = require('crypto');
6
- const iconv = require('iconv-lite');
7
-
8
- // Mock connection for testing
9
- let createMockConnection = (options = {}) => ({
10
- enabled: new Set(options.enabled || (options.utf8 ? ['UTF8=ACCEPT'] : [])),
11
- capabilities: new Map(options.capabilities || []),
12
- namespace: options.namespace || null
13
- });
14
-
15
- // ============================================
16
- // encodePath / decodePath tests
17
- // ============================================
18
-
19
- module.exports['Tools: encodePath with ASCII path'] = test => {
20
- let connection = createMockConnection();
21
- let result = tools.encodePath(connection, 'INBOX');
22
- test.equal(result, 'INBOX');
23
- test.done();
24
- };
25
-
26
- module.exports['Tools: encodePath with ASCII path (no UTF8)'] = test => {
27
- let connection = createMockConnection({ utf8: false });
28
- let result = tools.encodePath(connection, 'Sent/Gesendete');
29
- // ASCII path should remain unchanged
30
- test.equal(result, 'Sent/Gesendete');
31
- test.done();
32
- };
33
-
34
- module.exports['Tools: encodePath encodes Unicode to modified UTF-7 on rev1 (no UTF8)'] = test => {
35
- let connection = createMockConnection({ utf8: false });
36
- // 'õ' (U+00F5) -> UTF-16BE 00F5 -> modified-base64 'APU'
37
- test.equal(tools.encodePath(connection, 'Tõrva'), 'T&APU-rva');
38
- // a literal '&' must be escaped as '&-'
39
- test.equal(tools.encodePath(connection, 'Test&Folder'), 'Test&-Folder');
40
- test.done();
41
- };
42
-
43
- module.exports['Tools: encodePath with Unicode when UTF8=ACCEPT enabled'] = test => {
44
- let connection = createMockConnection({ utf8: true });
45
- let result = tools.encodePath(connection, 'Posteingang/Ordner');
46
- // With UTF8=ACCEPT, path should remain unchanged
47
- test.equal(result, 'Posteingang/Ordner');
48
- test.done();
49
- };
50
-
51
- module.exports['Tools: encodePath with null/undefined'] = test => {
52
- let connection = createMockConnection();
53
- test.equal(tools.encodePath(connection, null), '');
54
- test.equal(tools.encodePath(connection, undefined), '');
55
- test.done();
56
- };
57
-
58
- module.exports['Tools: decodePath with ASCII path'] = test => {
59
- let connection = createMockConnection();
60
- let result = tools.decodePath(connection, 'INBOX');
61
- test.equal(result, 'INBOX');
62
- test.done();
63
- };
64
-
65
- module.exports['Tools: decodePath with ampersand'] = test => {
66
- let connection = createMockConnection({ utf8: false });
67
- // modified UTF-7: '&-' is the escaped form of a literal ampersand
68
- test.equal(tools.decodePath(connection, 'Test&-Folder'), 'Test&Folder');
69
- // and an encoded sequence round-trips back to Unicode
70
- test.equal(tools.decodePath(connection, 'T&APU-rva'), 'Tõrva');
71
- test.done();
72
- };
73
-
74
- module.exports['Tools: decodePath with UTF8=ACCEPT enabled'] = test => {
75
- let connection = createMockConnection({ utf8: true });
76
- let result = tools.decodePath(connection, 'Test&Folder');
77
- // With UTF8=ACCEPT, should not decode
78
- test.equal(result, 'Test&Folder');
79
- test.done();
80
- };
81
-
82
- module.exports['Tools: decodePath with null/undefined'] = test => {
83
- let connection = createMockConnection();
84
- test.equal(tools.decodePath(connection, null), '');
85
- test.equal(tools.decodePath(connection, undefined), '');
86
- test.done();
87
- };
88
-
89
- // ============================================
90
- // normalizePath tests
91
- // ============================================
92
-
93
- module.exports['Tools: normalizePath with INBOX (case insensitive)'] = test => {
94
- let connection = createMockConnection();
95
- test.equal(tools.normalizePath(connection, 'inbox'), 'INBOX');
96
- test.equal(tools.normalizePath(connection, 'INBOX'), 'INBOX');
97
- test.equal(tools.normalizePath(connection, 'InBox'), 'INBOX');
98
- test.done();
99
- };
100
-
101
- module.exports['Tools: normalizePath with array path'] = test => {
102
- let connection = createMockConnection({
103
- namespace: { delimiter: '/', prefix: '' }
104
- });
105
- let result = tools.normalizePath(connection, ['Folder', 'Subfolder']);
106
- test.equal(result, 'Folder/Subfolder');
107
- test.done();
108
- };
109
-
110
- module.exports['Tools: normalizePath with namespace prefix'] = test => {
111
- let connection = createMockConnection({
112
- namespace: { delimiter: '.', prefix: 'INBOX.' }
113
- });
114
- let result = tools.normalizePath(connection, 'Sent');
115
- test.equal(result, 'INBOX.Sent');
116
- test.done();
117
- };
118
-
119
- module.exports['Tools: normalizePath skip namespace'] = test => {
120
- let connection = createMockConnection({
121
- namespace: { delimiter: '.', prefix: 'INBOX.' }
122
- });
123
- let result = tools.normalizePath(connection, 'Sent', true);
124
- test.equal(result, 'Sent');
125
- test.done();
126
- };
127
-
128
- module.exports['Tools: normalizePath already has prefix'] = test => {
129
- let connection = createMockConnection({
130
- namespace: { delimiter: '.', prefix: 'INBOX.' }
131
- });
132
- let result = tools.normalizePath(connection, 'INBOX.Sent');
133
- test.equal(result, 'INBOX.Sent');
134
- test.done();
135
- };
136
-
137
- // ============================================
138
- // comparePaths tests
139
- // ============================================
140
-
141
- module.exports['Tools: comparePaths equal paths'] = test => {
142
- let connection = createMockConnection();
143
- test.equal(tools.comparePaths(connection, 'INBOX', 'INBOX'), true);
144
- test.equal(tools.comparePaths(connection, 'inbox', 'INBOX'), true);
145
- test.done();
146
- };
147
-
148
- module.exports['Tools: comparePaths different paths'] = test => {
149
- let connection = createMockConnection();
150
- test.equal(tools.comparePaths(connection, 'INBOX', 'Sent'), false);
151
- test.done();
152
- };
153
-
154
- module.exports['Tools: comparePaths with null/undefined'] = test => {
155
- let connection = createMockConnection();
156
- test.equal(tools.comparePaths(connection, null, 'INBOX'), false);
157
- test.equal(tools.comparePaths(connection, 'INBOX', null), false);
158
- test.equal(tools.comparePaths(connection, null, null), false);
159
- test.done();
160
- };
161
-
162
- // ============================================
163
- // updateCapabilities tests
164
- // ============================================
165
-
166
- module.exports['Tools: updateCapabilities with valid list'] = test => {
167
- let list = [{ value: 'IMAP4rev1' }, { value: 'IDLE' }, { value: 'NAMESPACE' }];
168
- let result = tools.updateCapabilities(list);
169
- test.ok(result instanceof Map);
170
- test.equal(result.get('IMAP4rev1'), true);
171
- test.equal(result.get('IDLE'), true);
172
- test.equal(result.get('NAMESPACE'), true);
173
- test.done();
174
- };
175
-
176
- module.exports['Tools: updateCapabilities normalizes IMAP4rev2 casing'] = test => {
177
- let list = [{ value: 'IMAP4rev2' }, { value: 'imap4rev1' }];
178
- let result = tools.updateCapabilities(list);
179
- // Wire tokens are uppercased, but the rev1/rev2 keys use the RFC spelling
180
- test.equal(result.get('IMAP4rev2'), true);
181
- test.equal(result.get('IMAP4rev1'), true);
182
- test.equal(result.has('IMAP4REV2'), false);
183
- test.done();
184
- };
185
-
186
- module.exports['Tools: isRev2Active for rev2-only server'] = test => {
187
- // rev2 without rev1 means rev2 is the base protocol, no ENABLE needed
188
- let connection = createMockConnection({ capabilities: [['IMAP4rev2', true]] });
189
- test.equal(tools.isRev2Active(connection), true);
190
- test.done();
191
- };
192
-
193
- module.exports['Tools: isRev2Active for dual server without ENABLE'] = test => {
194
- // Advertising both keeps the session in rev1 mode until ENABLE IMAP4rev2
195
- let connection = createMockConnection({
196
- capabilities: [
197
- ['IMAP4rev1', true],
198
- ['IMAP4rev2', true]
199
- ]
200
- });
201
- test.equal(tools.isRev2Active(connection), false);
202
- test.done();
203
- };
204
-
205
- module.exports['Tools: isRev2Active for dual server with ENABLE'] = test => {
206
- let connection = createMockConnection({
207
- capabilities: [
208
- ['IMAP4rev1', true],
209
- ['IMAP4rev2', true]
210
- ],
211
- enabled: ['IMAP4REV2']
212
- });
213
- test.equal(tools.isRev2Active(connection), true);
214
- test.done();
215
- };
216
-
217
- module.exports['Tools: isRev2Active for rev1-only server'] = test => {
218
- let connection = createMockConnection({ capabilities: [['IMAP4rev1', true]] });
219
- test.equal(tools.isRev2Active(connection), false);
220
- test.done();
221
- };
222
-
223
- module.exports['Tools: hasCapability with advertised token'] = test => {
224
- let connection = createMockConnection({
225
- capabilities: [
226
- ['IMAP4rev1', true],
227
- ['UIDPLUS', true]
228
- ]
229
- });
230
- test.equal(tools.hasCapability(connection, 'UIDPLUS'), true);
231
- test.equal(tools.hasCapability(connection, 'MOVE'), false);
232
- test.done();
233
- };
234
-
235
- module.exports['Tools: hasCapability folds extensions into active rev2'] = test => {
236
- let connection = createMockConnection({ capabilities: [['IMAP4rev2', true]] });
237
- // RFC 9051 Appendix E folds these into base IMAP4rev2 - the complete set
238
- for (let capability of [
239
- 'ENABLE',
240
- 'ESEARCH',
241
- 'IDLE',
242
- 'LIST-EXTENDED',
243
- 'LIST-STATUS',
244
- 'LITERAL-',
245
- 'MOVE',
246
- 'NAMESPACE',
247
- 'SASL-IR',
248
- 'SEARCHRES',
249
- 'SPECIAL-USE',
250
- 'STATUS=SIZE',
251
- 'UIDPLUS',
252
- 'UNSELECT'
253
- ]) {
254
- test.equal(tools.hasCapability(connection, capability), true, `${capability} should be folded into rev2`);
255
- }
256
- // BINARY is intentionally not folded
257
- test.equal(tools.hasCapability(connection, 'BINARY'), false);
258
- test.done();
259
- };
260
-
261
- module.exports['Tools: hasCapability does not fold on unenabled dual server'] = test => {
262
- let connection = createMockConnection({
263
- capabilities: [
264
- ['IMAP4rev1', true],
265
- ['IMAP4rev2', true]
266
- ]
267
- });
268
- // Session is in rev1 mode - only explicitly advertised tokens count
269
- test.equal(tools.hasCapability(connection, 'UIDPLUS'), false);
270
- test.done();
271
- };
272
-
273
- module.exports['Tools: encodePath keeps UTF-8 when rev2 is active'] = test => {
274
- let connection = createMockConnection({ capabilities: [['IMAP4rev2', true]] });
275
- // rev2 mailbox names are native UTF-8, modified UTF-7 must not be applied
276
- test.equal(tools.encodePath(connection, 'T\u00f5rva'), 'T\u00f5rva');
277
- test.done();
278
- };
279
-
280
- module.exports['Tools: decodePath keeps ampersand sequences when rev2 is active'] = test => {
281
- let connection = createMockConnection({ capabilities: [['IMAP4rev2', true]] });
282
- // Under rev2 an "&"-sequence is a literal name, not modified UTF-7
283
- test.equal(tools.decodePath(connection, 'A&AOQ-B'), 'A&AOQ-B');
284
- test.done();
285
- };
286
-
287
- module.exports['Tools: updateCapabilities with APPENDLIMIT'] = test => {
288
- let list = [{ value: 'APPENDLIMIT=52428800' }];
289
- let result = tools.updateCapabilities(list);
290
- test.equal(result.get('APPENDLIMIT'), 52428800);
291
- test.done();
292
- };
293
-
294
- module.exports['Tools: updateCapabilities with empty/null list'] = test => {
295
- test.ok(tools.updateCapabilities(null) instanceof Map);
296
- test.ok(tools.updateCapabilities([]) instanceof Map);
297
- test.ok(tools.updateCapabilities(undefined) instanceof Map);
298
- test.done();
299
- };
300
-
301
- module.exports['Tools: updateCapabilities skips non-string values'] = test => {
302
- let list = [{ value: 'IDLE' }, { value: 123 }, { value: null }];
303
- let result = tools.updateCapabilities(list);
304
- test.equal(result.get('IDLE'), true);
305
- test.equal(result.size, 1);
306
- test.done();
307
- };
308
-
309
- // ============================================
310
- // getStatusCode tests
311
- // ============================================
312
-
313
- module.exports['Tools: getStatusCode with valid response'] = test => {
314
- let response = {
315
- attributes: [
316
- {
317
- section: [{ value: 'TRYCREATE' }]
318
- }
319
- ]
320
- };
321
- test.equal(tools.getStatusCode(response), 'TRYCREATE');
322
- test.done();
323
- };
324
-
325
- module.exports['Tools: getStatusCode with null/invalid response'] = test => {
326
- test.equal(tools.getStatusCode(null), false);
327
- test.equal(tools.getStatusCode({}), false);
328
- test.equal(tools.getStatusCode({ attributes: [] }), false);
329
- test.equal(tools.getStatusCode({ attributes: [{}] }), false);
330
- test.done();
331
- };
332
-
333
- // ============================================
334
- // getErrorText tests
335
- // ============================================
336
-
337
- module.exports['Tools: getErrorText with null response'] = async test => {
338
- let result = await tools.getErrorText(null);
339
- test.equal(result, false);
340
- test.done();
341
- };
342
-
343
- module.exports['Tools: getErrorText with valid response'] = async test => {
344
- let response = {
345
- tag: '*',
346
- command: 'OK',
347
- attributes: [{ type: 'TEXT', value: 'Success' }]
348
- };
349
- let result = await tools.getErrorText(response);
350
- test.ok(typeof result === 'string');
351
- test.done();
352
- };
353
-
354
- module.exports['Tools: getErrorText survives a response that cannot be re-encoded'] = async test => {
355
- // The parser tolerates stray bytes inside an OK/NO/BAD atom, and those have no
356
- // valid IMAP string encoding. The error text is diagnostic, so it must still be
357
- // produced rather than replacing the server's error with an encoding failure.
358
- let response = await parser(Buffer.from('A1 NO [SERVERBUG\x00X] it failed', 'binary'));
359
- let result = await tools.getErrorText(response);
360
- test.ok(typeof result === 'string', 'error text should still be produced');
361
- test.ok(result.includes('it failed'), 'the human-readable part must survive');
362
- test.done();
363
- };
364
-
365
- // ============================================
366
- // getFlagColor tests
367
- // ============================================
368
-
369
- module.exports['Tools: getFlagColor without Flagged'] = test => {
370
- let flags = new Set(['\\Seen']);
371
- test.equal(tools.getFlagColor(flags), null);
372
- test.done();
373
- };
374
-
375
- module.exports['Tools: getFlagColor with Flagged only (red)'] = test => {
376
- let flags = new Set(['\\Flagged']);
377
- test.equal(tools.getFlagColor(flags), 'red');
378
- test.done();
379
- };
380
-
381
- module.exports['Tools: getFlagColor with color bits'] = test => {
382
- // bit0=1, bit1=0, bit2=0 => orange (index 1)
383
- let flags = new Set(['\\Flagged', '$MailFlagBit0']);
384
- test.equal(tools.getFlagColor(flags), 'orange');
385
-
386
- // bit0=0, bit1=1, bit2=0 => yellow (index 2)
387
- flags = new Set(['\\Flagged', '$MailFlagBit1']);
388
- test.equal(tools.getFlagColor(flags), 'yellow');
389
-
390
- // bit0=1, bit1=1, bit2=0 => green (index 3)
391
- flags = new Set(['\\Flagged', '$MailFlagBit0', '$MailFlagBit1']);
392
- test.equal(tools.getFlagColor(flags), 'green');
393
-
394
- // bit0=0, bit1=0, bit2=1 => blue (index 4)
395
- flags = new Set(['\\Flagged', '$MailFlagBit2']);
396
- test.equal(tools.getFlagColor(flags), 'blue');
397
-
398
- // bit0=1, bit1=0, bit2=1 => purple (index 5)
399
- flags = new Set(['\\Flagged', '$MailFlagBit0', '$MailFlagBit2']);
400
- test.equal(tools.getFlagColor(flags), 'purple');
401
-
402
- // bit0=0, bit1=1, bit2=1 => grey (index 6)
403
- flags = new Set(['\\Flagged', '$MailFlagBit1', '$MailFlagBit2']);
404
- test.equal(tools.getFlagColor(flags), 'grey');
405
-
406
- test.done();
407
- };
408
-
409
- module.exports['Tools: getFlagColor with all bits set (index 7) defaults to red'] = test => {
410
- // bit0=1, bit1=2, bit2=4 => color=7, FLAG_COLORS[7] is undefined => defaults to 'red'
411
- let flags = new Set(['\\Flagged', '$MailFlagBit0', '$MailFlagBit1', '$MailFlagBit2']);
412
- test.equal(tools.getFlagColor(flags), 'red');
413
- test.done();
414
- };
415
-
416
- // ============================================
417
- // getColorFlags tests
418
- // ============================================
419
-
420
- module.exports['Tools: getColorFlags with valid color'] = test => {
421
- // 'orange' is index 1, which is truthy
422
- let result = tools.getColorFlags('orange');
423
- test.ok(Array.isArray(result.add));
424
- test.ok(Array.isArray(result.remove));
425
- test.ok(result.add.includes('\\Flagged'));
426
- test.done();
427
- };
428
-
429
- module.exports['Tools: getColorFlags with red (index 0)'] = test => {
430
- // 'red' is index 0 — should add \\Flagged and remove all MailFlagBit flags
431
- let result = tools.getColorFlags('red');
432
- test.ok(Array.isArray(result.add));
433
- test.ok(Array.isArray(result.remove));
434
- test.ok(result.add.includes('\\Flagged'));
435
- test.done();
436
- };
437
-
438
- module.exports['Tools: getColorFlags with null (remove flag)'] = test => {
439
- let result = tools.getColorFlags(null);
440
- test.ok(result.remove.includes('\\Flagged'));
441
- test.done();
442
- };
443
-
444
- module.exports['Tools: getColorFlags with invalid color'] = test => {
445
- let result = tools.getColorFlags('invalid-color');
446
- test.equal(result, null);
447
- test.done();
448
- };
449
-
450
- module.exports['Tools: getColorFlags sets correct bits'] = test => {
451
- // orange = index 1 = bit0 set
452
- let result = tools.getColorFlags('orange');
453
- test.ok(result.add.includes('$MailFlagBit0'));
454
- test.ok(result.remove.includes('$MailFlagBit1'));
455
- test.ok(result.remove.includes('$MailFlagBit2'));
456
-
457
- // green = index 3 = bit0 + bit1 set
458
- result = tools.getColorFlags('green');
459
- test.ok(result.add.includes('$MailFlagBit0'));
460
- test.ok(result.add.includes('$MailFlagBit1'));
461
- test.ok(result.remove.includes('$MailFlagBit2'));
462
-
463
- test.done();
464
- };
465
-
466
- // ============================================
467
- // isDate tests
468
- // ============================================
469
-
470
- module.exports['Tools: isDate with Date object'] = test => {
471
- test.equal(tools.isDate(new Date()), true);
472
- test.equal(tools.isDate(new Date('2023-01-01')), true);
473
- test.done();
474
- };
475
-
476
- module.exports['Tools: isDate with non-Date'] = test => {
477
- test.equal(tools.isDate('2023-01-01'), false);
478
- test.equal(tools.isDate(12345), false);
479
- test.equal(tools.isDate(null), false);
480
- test.equal(tools.isDate({}), false);
481
- test.done();
482
- };
483
-
484
- // ============================================
485
- // formatDate tests
486
- // ============================================
487
-
488
- module.exports['Tools: formatDate with Date object'] = test => {
489
- let date = new Date('2023-06-15T00:00:00.000Z');
490
- let result = tools.formatDate(date);
491
- test.equal(result, '15-Jun-2023');
492
- test.done();
493
- };
494
-
495
- module.exports['Tools: formatDate with string'] = test => {
496
- let result = tools.formatDate('2023-06-15');
497
- test.equal(result, '15-Jun-2023');
498
- test.done();
499
- };
500
-
501
- module.exports['Tools: formatDate with invalid date'] = test => {
502
- let result = tools.formatDate('invalid');
503
- test.equal(result, undefined);
504
- test.done();
505
- };
506
-
507
- // ============================================
508
- // formatDateTime tests
509
- // ============================================
510
-
511
- module.exports['Tools: formatDateTime with Date object'] = test => {
512
- let date = new Date('2023-06-15T14:30:45.000Z');
513
- let result = tools.formatDateTime(date);
514
- test.ok(result.includes('Jun-2023'));
515
- test.ok(result.includes('14:30:45'));
516
- test.ok(result.includes('+0000'));
517
- test.done();
518
- };
519
-
520
- module.exports['Tools: formatDateTime with null/undefined'] = test => {
521
- test.equal(tools.formatDateTime(null), undefined);
522
- test.equal(tools.formatDateTime(undefined), undefined);
523
- test.done();
524
- };
525
-
526
- module.exports['Tools: formatDateTime with string'] = test => {
527
- let result = tools.formatDateTime('2023-06-15T10:00:00Z');
528
- test.ok(typeof result === 'string');
529
- test.ok(result.includes('Jun-2023'));
530
- test.done();
531
- };
532
-
533
- module.exports['Tools: formatDateTime with invalid date string'] = test => {
534
- let result = tools.formatDateTime('invalid-date-string');
535
- test.equal(result, undefined);
536
- test.done();
537
- };
538
-
539
- // ============================================
540
- // formatFlag tests
541
- // ============================================
542
-
543
- module.exports['Tools: formatFlag with standard flags'] = test => {
544
- test.equal(tools.formatFlag('\\Seen'), '\\Seen');
545
- test.equal(tools.formatFlag('\\SEEN'), '\\Seen');
546
- test.equal(tools.formatFlag('\\answered'), '\\Answered');
547
- test.equal(tools.formatFlag('\\flagged'), '\\Flagged');
548
- test.equal(tools.formatFlag('\\deleted'), '\\Deleted');
549
- test.equal(tools.formatFlag('\\draft'), '\\Draft');
550
- test.done();
551
- };
552
-
553
- module.exports['Tools: formatFlag with Recent (cannot set)'] = test => {
554
- test.equal(tools.formatFlag('\\Recent'), false);
555
- test.equal(tools.formatFlag('\\recent'), false);
556
- test.done();
557
- };
558
-
559
- module.exports['Tools: formatFlag with custom flags'] = test => {
560
- test.equal(tools.formatFlag('$CustomFlag'), '$CustomFlag');
561
- test.equal(tools.formatFlag('MyFlag'), 'MyFlag');
562
- test.done();
563
- };
564
-
565
- // ============================================
566
- // canUseFlag tests
567
- // ============================================
568
-
569
- module.exports['Tools: canUseFlag with no mailbox'] = test => {
570
- test.equal(tools.canUseFlag(null, '\\Seen'), true);
571
- test.done();
572
- };
573
-
574
- module.exports['Tools: canUseFlag with wildcard permanent flags'] = test => {
575
- let mailbox = { permanentFlags: new Set(['\\*']) };
576
- test.equal(tools.canUseFlag(mailbox, '\\Seen'), true);
577
- test.equal(tools.canUseFlag(mailbox, '$CustomFlag'), true);
578
- test.done();
579
- };
580
-
581
- module.exports['Tools: canUseFlag with specific permanent flags'] = test => {
582
- let mailbox = { permanentFlags: new Set(['\\Seen', '\\Flagged']) };
583
- test.equal(tools.canUseFlag(mailbox, '\\Seen'), true);
584
- test.equal(tools.canUseFlag(mailbox, '\\Flagged'), true);
585
- test.equal(tools.canUseFlag(mailbox, '\\Deleted'), false);
586
- test.done();
587
- };
588
-
589
- module.exports['Tools: canUseFlag with no permanent flags'] = test => {
590
- let mailbox = { permanentFlags: null };
591
- test.equal(tools.canUseFlag(mailbox, '\\Seen'), true);
592
- test.done();
593
- };
594
-
595
- // ============================================
596
- // expandRange tests
597
- // ============================================
598
-
599
- module.exports['Tools: expandRange with single values'] = test => {
600
- let result = tools.expandRange('1,2,3');
601
- test.deepEqual(result, [1, 2, 3]);
602
- test.done();
603
- };
604
-
605
- module.exports['Tools: expandRange with range'] = test => {
606
- let result = tools.expandRange('1:5');
607
- test.deepEqual(result, [1, 2, 3, 4, 5]);
608
- test.done();
609
- };
610
-
611
- module.exports['Tools: expandRange with reverse range'] = test => {
612
- let result = tools.expandRange('5:1');
613
- test.deepEqual(result, [5, 4, 3, 2, 1]);
614
- test.done();
615
- };
616
-
617
- module.exports['Tools: expandRange with mixed'] = test => {
618
- let result = tools.expandRange('1,3:5,10');
619
- test.deepEqual(result, [1, 3, 4, 5, 10]);
620
- test.done();
621
- };
622
-
623
- module.exports['Tools: expandRange with same start/end'] = test => {
624
- let result = tools.expandRange('5:5');
625
- test.deepEqual(result, [5]);
626
- test.done();
627
- };
628
-
629
- module.exports['Tools: expandRange skips entries that are not valid nz-numbers'] = test => {
630
- // Server-supplied garbage must not produce bogus ids or endless loops
631
- test.deepEqual(tools.expandRange('Infinity:5'), []);
632
- test.deepEqual(tools.expandRange('0:3'), []);
633
- test.deepEqual(tools.expandRange('abc,4,1:x'), [4]);
634
- test.deepEqual(tools.expandRange('*'), []);
635
- test.deepEqual(tools.expandRange('4294967296'), []);
636
- test.done();
637
- };
638
-
639
- module.exports['Tools: expandRange caps hostile range spans'] = test => {
640
- // A hostile range like 1:4294967295 is cut off at the expansion limit
641
- // instead of exhausting memory
642
- let result = tools.expandRange('1:4294967295');
643
- test.equal(result.length, 0x1000000);
644
- test.equal(result[0], 1);
645
- test.equal(result[result.length - 1], 0x1000000);
646
-
647
- let reverse = tools.expandRange('4294967295:4278190080');
648
- test.equal(reverse.length, 0x1000000);
649
- test.equal(reverse[0], 4294967295);
650
- test.done();
651
- };
652
-
653
- // ============================================
654
- // packMessageRange tests
655
- // ============================================
656
-
657
- module.exports['Tools: packMessageRange with sequential numbers'] = test => {
658
- let result = tools.packMessageRange([1, 2, 3, 4, 5]);
659
- test.equal(result, '1:5');
660
- test.done();
661
- };
662
-
663
- module.exports['Tools: packMessageRange with gaps'] = test => {
664
- let result = tools.packMessageRange([1, 2, 3, 7, 8, 9]);
665
- test.equal(result, '1:3,7:9');
666
- test.done();
667
- };
668
-
669
- module.exports['Tools: packMessageRange with single values'] = test => {
670
- let result = tools.packMessageRange([1, 5, 10]);
671
- test.equal(result, '1,5,10');
672
- test.done();
673
- };
674
-
675
- module.exports['Tools: packMessageRange with unsorted input'] = test => {
676
- let result = tools.packMessageRange([5, 1, 3, 2, 4]);
677
- test.equal(result, '1:5');
678
- test.done();
679
- };
680
-
681
- module.exports['Tools: packMessageRange with empty array'] = test => {
682
- test.equal(tools.packMessageRange([]), '');
683
- test.done();
684
- };
685
-
686
- module.exports['Tools: packMessageRange with non-array'] = test => {
687
- test.equal(tools.packMessageRange(5), '5');
688
- test.equal(tools.packMessageRange(null), '');
689
- test.done();
690
- };
691
-
692
- // ============================================
693
- // processName tests
694
- // ============================================
695
-
696
- module.exports['Tools: processName with quoted string'] = test => {
697
- test.equal(tools.processName('"John Doe"'), 'John Doe');
698
- test.done();
699
- };
700
-
701
- module.exports['Tools: processName with unquoted string'] = test => {
702
- test.equal(tools.processName('John Doe'), 'John Doe');
703
- test.done();
704
- };
705
-
706
- module.exports['Tools: processName with null/undefined'] = test => {
707
- test.equal(tools.processName(null), '');
708
- test.equal(tools.processName(undefined), '');
709
- test.done();
710
- };
711
-
712
- module.exports['Tools: processName with short quoted'] = test => {
713
- // String too short to have quotes removed (less than 3 chars)
714
- test.equal(tools.processName('""'), '""');
715
- test.equal(tools.processName('"a"'), 'a');
716
- test.done();
717
- };
718
-
719
- // ============================================
720
- // decodeText tests
721
- // ============================================
722
-
723
- module.exports['Tools: decodeText decodes encoded words and strips quotes'] = test => {
724
- test.equal(tools.decodeText('=?utf-8?Q?T=C3=B5nu?='), 'Tõnu');
725
- test.equal(tools.decodeText('"=?utf-8?Q?T=C3=B5nu?="'), 'Tõnu');
726
- test.equal(tools.decodeText('Plain Name'), 'Plain Name');
727
- test.done();
728
- };
729
-
730
- module.exports['Tools: decodeText tolerates missing values'] = test => {
731
- // getStrValue returns false for a NIL envelope field
732
- test.equal(tools.decodeText(false), '');
733
- test.equal(tools.decodeText(null), '');
734
- test.equal(tools.decodeText(undefined), '');
735
- test.done();
736
- };
737
-
738
- // ============================================
739
- // getFolderTree tests
740
- // ============================================
741
-
742
- module.exports['Tools: getFolderTree with flat folders'] = test => {
743
- let folders = [
744
- { name: 'INBOX', path: 'INBOX', flags: new Set(), parent: [] },
745
- { name: 'Sent', path: 'Sent', flags: new Set(), parent: [] }
746
- ];
747
- let tree = tools.getFolderTree(folders);
748
-
749
- test.ok(tree.root);
750
- test.ok(Array.isArray(tree.folders));
751
- test.equal(tree.folders.length, 2);
752
- test.done();
753
- };
754
-
755
- module.exports['Tools: getFolderTree with nested folders'] = test => {
756
- let folders = [
757
- { name: 'INBOX', path: 'INBOX', flags: new Set(['\\HasChildren']), parent: [] },
758
- { name: 'Work', path: 'INBOX/Work', flags: new Set(), parent: ['INBOX'] }
759
- ];
760
- let tree = tools.getFolderTree(folders);
761
-
762
- test.ok(tree.root);
763
- test.equal(tree.folders.length, 1);
764
- test.equal(tree.folders[0].name, 'INBOX');
765
- test.ok(Array.isArray(tree.folders[0].folders));
766
- test.done();
767
- };
768
-
769
- module.exports['Tools: getFolderTree with Noselect flag'] = test => {
770
- let folders = [{ name: 'Archive', path: 'Archive', flags: new Set(['\\Noselect']), parent: [] }];
771
- let tree = tools.getFolderTree(folders);
772
-
773
- test.equal(tree.folders[0].disabled, true);
774
- test.done();
775
- };
776
-
777
- module.exports['Tools: getFolderTree with specialUse'] = test => {
778
- let folders = [{ name: 'Sent', path: 'Sent', flags: new Set(), parent: [], specialUse: '\\Sent' }];
779
- let tree = tools.getFolderTree(folders);
780
-
781
- test.equal(tree.folders[0].specialUse, '\\Sent');
782
- test.done();
783
- };
784
-
785
- module.exports['Tools: getFolderTree with delimiter'] = test => {
786
- let folders = [{ name: 'Folder', path: 'Folder', flags: new Set(), parent: [], delimiter: '/' }];
787
- let tree = tools.getFolderTree(folders);
788
-
789
- test.equal(tree.folders[0].delimiter, '/');
790
- test.done();
791
- };
792
-
793
- module.exports['Tools: getFolderTree updates existing entries'] = test => {
794
- let folders = [
795
- { name: 'INBOX', path: 'INBOX', flags: new Set(['\\HasChildren']), parent: [], listed: true },
796
- { name: 'INBOX', path: 'INBOX', flags: new Set(['\\HasChildren']), parent: [], subscribed: true }
797
- ];
798
- let tree = tools.getFolderTree(folders);
799
-
800
- // Should update the existing entry, not create duplicate
801
- test.equal(tree.folders.length, 1);
802
- test.done();
803
- };
804
-
805
- // ============================================
806
- // parseEnvelope tests
807
- // ============================================
808
-
809
- module.exports['Tools: parseEnvelope with complete envelope'] = test => {
810
- let entry = [
811
- { value: 'Mon, 15 Jun 2023 10:00:00 +0000' }, // date
812
- { value: 'Test Subject' }, // subject
813
- [[{ value: 'Sender Name' }, null, { value: 'sender' }, { value: 'example.com' }]], // from
814
- [[{ value: 'Sender Name' }, null, { value: 'sender' }, { value: 'example.com' }]], // sender
815
- [[{ value: 'Reply Name' }, null, { value: 'reply' }, { value: 'example.com' }]], // reply-to
816
- [[{ value: 'To Name' }, null, { value: 'to' }, { value: 'example.com' }]], // to
817
- [[{ value: 'CC Name' }, null, { value: 'cc' }, { value: 'example.com' }]], // cc
818
- [[{ value: 'BCC Name' }, null, { value: 'bcc' }, { value: 'example.com' }]], // bcc
819
- { value: '<reply-id@example.com>' }, // in-reply-to
820
- { value: '<message-id@example.com>' } // message-id
821
- ];
822
-
823
- let result = tools.parseEnvelope(entry);
824
-
825
- test.ok(result.date instanceof Date);
826
- test.equal(result.subject, 'Test Subject');
827
- test.equal(result.from[0].address, 'sender@example.com');
828
- test.equal(result.to[0].address, 'to@example.com');
829
- test.equal(result.messageId, '<message-id@example.com>');
830
- test.done();
831
- };
832
-
833
- module.exports['Tools: parseEnvelope with minimal envelope'] = test => {
834
- let entry = [
835
- null, // date
836
- null, // subject
837
- [], // from
838
- [], // sender
839
- [], // reply-to
840
- [], // to
841
- [], // cc
842
- [], // bcc
843
- null, // in-reply-to
844
- null // message-id
845
- ];
846
-
847
- let result = tools.parseEnvelope(entry);
848
- test.ok(typeof result === 'object');
849
- test.equal(result.subject, undefined);
850
- test.done();
851
- };
852
-
853
- module.exports['Tools: parseEnvelope with invalid date'] = test => {
854
- let entry = [{ value: 'invalid-date' }, null, null, null, null, null, null, null, null, null];
855
-
856
- let result = tools.parseEnvelope(entry);
857
- test.equal(result.date, 'invalid-date');
858
- test.done();
859
- };
860
-
861
- module.exports['Tools: parseEnvelope with Buffer value'] = test => {
862
- let entry = [
863
- { value: Buffer.from('Mon, 15 Jun 2023 10:00:00 +0000') }, // date as Buffer
864
- { value: Buffer.from('Buffer Subject') }, // subject as Buffer
865
- [[{ value: Buffer.from('Sender Name') }, null, { value: Buffer.from('sender') }, { value: Buffer.from('example.com') }]], // from with Buffers
866
- [], // sender
867
- [], // reply-to
868
- [], // to
869
- [], // cc
870
- [], // bcc
871
- null, // in-reply-to
872
- { value: Buffer.from('<msg-id@example.com>') } // message-id as Buffer
873
- ];
874
-
875
- let result = tools.parseEnvelope(entry);
876
- test.equal(result.subject, 'Buffer Subject');
877
- test.equal(result.from[0].address, 'sender@example.com');
878
- test.equal(result.messageId, '<msg-id@example.com>');
879
- test.done();
880
- };
881
-
882
- module.exports['Tools: parseEnvelope with empty address parts'] = test => {
883
- // When both local part and domain are null/empty, address should be empty string
884
- let entry = [
885
- null, // date
886
- null, // subject
887
- [[{ value: 'Group Name' }, null, null, null]], // from with no email parts (group syntax)
888
- [], // sender
889
- [], // reply-to
890
- [], // to
891
- [], // cc
892
- [], // bcc
893
- null, // in-reply-to
894
- null // message-id
895
- ];
896
-
897
- let result = tools.parseEnvelope(entry);
898
- // Address '@' should be converted to empty string and filtered out if no name
899
- test.ok(result.from);
900
- // The entry has a name but no valid address, should still be included with empty address
901
- test.equal(result.from.length, 1);
902
- test.equal(result.from[0].name, 'Group Name');
903
- test.equal(result.from[0].address, '');
904
- test.done();
905
- };
906
-
907
- module.exports['Tools: parseEnvelope keeps group syntax out of the address'] = test => {
908
- // RFC 9051 7.5.2: a NIL host marks group syntax, so "undisclosed-recipients:;" must
909
- // not turn into the invented address "undisclosed-recipients@"
910
- let entry = [
911
- null, // date
912
- null, // subject
913
- [], // from
914
- [], // sender
915
- [], // reply-to
916
- [
917
- [null, null, { value: 'undisclosed-recipients' }, null], // start of group
918
- [null, null, null, null] // end of group
919
- ], // to
920
- [], // cc
921
- [], // bcc
922
- null, // in-reply-to
923
- null // message-id
924
- ];
925
-
926
- let result = tools.parseEnvelope(entry);
927
- // The end-of-group marker carries neither name nor address and is dropped
928
- test.deepEqual(result.to, [{ name: 'undisclosed-recipients', address: '' }]);
929
- test.done();
930
- };
931
-
932
- module.exports['Tools: parseEnvelope keeps group members alongside the markers'] = test => {
933
- let entry = [
934
- null, // date
935
- null, // subject
936
- [], // from
937
- [], // sender
938
- [], // reply-to
939
- [
940
- [null, null, { value: 'Team' }, null], // start of group
941
- [{ value: 'Member One' }, null, { value: 'one' }, { value: 'example.com' }],
942
- [{ value: 'Member Two' }, null, { value: 'two' }, { value: 'example.com' }],
943
- [null, null, null, null] // end of group
944
- ], // to
945
- [], // cc
946
- [], // bcc
947
- null, // in-reply-to
948
- null // message-id
949
- ];
950
-
951
- let result = tools.parseEnvelope(entry);
952
- test.deepEqual(result.to, [
953
- { name: 'Team', address: '' },
954
- { name: 'Member One', address: 'one@example.com' },
955
- { name: 'Member Two', address: 'two@example.com' }
956
- ]);
957
- test.done();
958
- };
959
-
960
- module.exports['Tools: parseEnvelope does not join a NIL host onto a mailbox'] = test => {
961
- // Some servers parse a malformed header such as
962
- // "To: user@example.com user@example.com" into a personal name plus a mailbox
963
- // with a NIL host. Joining those produced the invalid address "example.com@".
964
- let entry = [
965
- null, // date
966
- null, // subject
967
- [], // from
968
- [], // sender
969
- [], // reply-to
970
- [[{ value: 'user@example.com user@' }, null, { value: 'example.com' }, null]], // to
971
- [], // cc
972
- [], // bcc
973
- null, // in-reply-to
974
- null // message-id
975
- ];
976
-
977
- let result = tools.parseEnvelope(entry);
978
- test.deepEqual(result.to, [{ name: 'user@example.com user@', address: '' }]);
979
- test.done();
980
- };
981
-
982
- module.exports['Tools: parseEnvelope decodes an encoded group name'] = test => {
983
- let entry = [
984
- null, // date
985
- null, // subject
986
- [], // from
987
- [], // sender
988
- [], // reply-to
989
- [[null, null, { value: '=?utf-8?Q?T=C3=B5ny?=' }, null]], // to
990
- [], // cc
991
- [], // bcc
992
- null, // in-reply-to
993
- null // message-id
994
- ];
995
-
996
- let result = tools.parseEnvelope(entry);
997
- test.deepEqual(result.to, [{ name: 'Tõny', address: '' }]);
998
- test.done();
999
- };
1000
-
1001
- // ============================================
1002
- // getStructuredParams tests
1003
- // ============================================
1004
-
1005
- module.exports['Tools: getStructuredParams with simple params'] = test => {
1006
- let arr = [{ value: 'charset' }, { value: 'utf-8' }, { value: 'name' }, { value: 'file.txt' }];
1007
-
1008
- let result = tools.getStructuredParams(arr);
1009
- test.equal(result.charset, 'utf-8');
1010
- test.equal(result.name, 'file.txt');
1011
- test.done();
1012
- };
1013
-
1014
- module.exports['Tools: getStructuredParams with null'] = test => {
1015
- let result = tools.getStructuredParams(null);
1016
- test.deepEqual(result, {});
1017
- test.done();
1018
- };
1019
-
1020
- module.exports['Tools: getStructuredParams with continuation'] = test => {
1021
- // RFC 2231 continuation
1022
- let arr = [{ value: 'filename*0' }, { value: 'very' }, { value: 'filename*1' }, { value: 'long' }, { value: 'filename*2' }, { value: 'name.txt' }];
1023
-
1024
- let result = tools.getStructuredParams(arr);
1025
- test.equal(result.filename, 'verylongname.txt');
1026
- test.done();
1027
- };
1028
-
1029
- // ============================================
1030
- // parseBodystructure tests
1031
- // ============================================
1032
-
1033
- module.exports['Tools: parseBodystructure with simple text'] = test => {
1034
- let entry = [
1035
- { value: 'TEXT' },
1036
- { value: 'PLAIN' },
1037
- [{ value: 'CHARSET' }, { value: 'UTF-8' }],
1038
- null, // id
1039
- null, // description
1040
- { value: '7BIT' }, // encoding
1041
- { value: '1234' }, // size
1042
- { value: '50' } // lines
1043
- ];
1044
-
1045
- let result = tools.parseBodystructure(entry);
1046
- test.equal(result.type, 'text/plain');
1047
- test.equal(result.encoding, '7bit');
1048
- test.equal(result.size, 1234);
1049
- test.equal(result.parameters.charset, 'UTF-8');
1050
- test.done();
1051
- };
1052
-
1053
- module.exports['Tools: parseBodystructure with multipart'] = test => {
1054
- let textPart = [{ value: 'TEXT' }, { value: 'PLAIN' }, null, null, null, { value: '7BIT' }, { value: '100' }, { value: '5' }];
1055
-
1056
- let htmlPart = [{ value: 'TEXT' }, { value: 'HTML' }, null, null, null, { value: 'QUOTED-PRINTABLE' }, { value: '200' }, { value: '10' }];
1057
-
1058
- let entry = [textPart, htmlPart, { value: 'ALTERNATIVE' }];
1059
-
1060
- let result = tools.parseBodystructure(entry);
1061
- test.equal(result.type, 'multipart/alternative');
1062
- test.ok(Array.isArray(result.childNodes));
1063
- test.equal(result.childNodes.length, 2);
1064
- test.equal(result.childNodes[0].type, 'text/plain');
1065
- test.equal(result.childNodes[1].type, 'text/html');
1066
- test.done();
1067
- };
1068
-
1069
- module.exports['Tools: parseBodystructure with attachment'] = test => {
1070
- let entry = [
1071
- { value: 'APPLICATION' },
1072
- { value: 'PDF' },
1073
- [{ value: 'NAME' }, { value: 'document.pdf' }],
1074
- null,
1075
- null,
1076
- { value: 'BASE64' },
1077
- { value: '50000' }
1078
- ];
1079
-
1080
- let result = tools.parseBodystructure(entry);
1081
- test.equal(result.type, 'application/pdf');
1082
- test.equal(result.parameters.name, 'document.pdf');
1083
- test.done();
1084
- };
1085
-
1086
- module.exports['Tools: parseBodystructure with md5'] = test => {
1087
- // Non-text type with extension data including md5
1088
- let entry = [
1089
- { value: 'APPLICATION' },
1090
- { value: 'OCTET-STREAM' },
1091
- null, // params
1092
- null, // id
1093
- null, // description
1094
- { value: 'BASE64' }, // encoding
1095
- { value: '1000' }, // size
1096
- { value: 'd41d8cd98f00b204e9800998ecf8427e' }, // md5
1097
- null, // disposition
1098
- null // language (to ensure we have enough elements)
1099
- ];
1100
-
1101
- let result = tools.parseBodystructure(entry);
1102
- test.equal(result.type, 'application/octet-stream');
1103
- test.equal(result.md5, 'd41d8cd98f00b204e9800998ecf8427e');
1104
- test.done();
1105
- };
1106
-
1107
- module.exports['Tools: parseBodystructure with language'] = test => {
1108
- // Non-text type with language extension
1109
- let entry = [
1110
- { value: 'APPLICATION' },
1111
- { value: 'PDF' },
1112
- null, // params
1113
- null, // id
1114
- null, // description
1115
- { value: 'BASE64' }, // encoding
1116
- { value: '5000' }, // size
1117
- null, // md5
1118
- null, // disposition
1119
- [{ value: 'EN' }, { value: 'DE' }], // language (array of values)
1120
- null // location (to ensure enough elements)
1121
- ];
1122
-
1123
- let result = tools.parseBodystructure(entry);
1124
- test.equal(result.type, 'application/pdf');
1125
- test.ok(Array.isArray(result.language));
1126
- test.deepEqual(result.language, ['en', 'de']);
1127
- test.done();
1128
- };
1129
-
1130
- module.exports['Tools: parseBodystructure with location'] = test => {
1131
- // Non-text type with location extension
1132
- let entry = [
1133
- { value: 'IMAGE' },
1134
- { value: 'PNG' },
1135
- null, // params
1136
- null, // id
1137
- null, // description
1138
- { value: 'BASE64' }, // encoding
1139
- { value: '10000' }, // size
1140
- null, // md5
1141
- null, // disposition
1142
- null, // language
1143
- { value: 'http://example.com/image.png' }, // location
1144
- null // extra element to ensure we have enough
1145
- ];
1146
-
1147
- let result = tools.parseBodystructure(entry);
1148
- test.equal(result.type, 'image/png');
1149
- test.equal(result.location, 'http://example.com/image.png');
1150
- test.done();
1151
- };
1152
-
1153
- module.exports['Tools: parseBodystructure with all extension fields'] = test => {
1154
- // Non-text type with all extension fields
1155
- let entry = [
1156
- { value: 'APPLICATION' },
1157
- { value: 'ZIP' },
1158
- [{ value: 'NAME' }, { value: 'archive.zip' }], // params
1159
- { value: '<id123@example.com>' }, // id
1160
- { value: 'A zip archive' }, // description
1161
- { value: 'BASE64' }, // encoding
1162
- { value: '50000' }, // size
1163
- { value: 'abc123def456' }, // md5
1164
- [{ value: 'ATTACHMENT' }, [{ value: 'FILENAME' }, { value: 'archive.zip' }]], // disposition with params
1165
- [{ value: 'EN' }], // language
1166
- { value: 'http://example.com/archive.zip' }, // location
1167
- null // extra element
1168
- ];
1169
-
1170
- let result = tools.parseBodystructure(entry);
1171
- test.equal(result.type, 'application/zip');
1172
- test.equal(result.parameters.name, 'archive.zip');
1173
- test.equal(result.id, '<id123@example.com>');
1174
- test.equal(result.description, 'A zip archive');
1175
- test.equal(result.encoding, 'base64');
1176
- test.equal(result.size, 50000);
1177
- test.equal(result.md5, 'abc123def456');
1178
- test.equal(result.disposition, 'attachment');
1179
- test.equal(result.dispositionParameters.filename, 'archive.zip');
1180
- test.deepEqual(result.language, ['en']);
1181
- test.equal(result.location, 'http://example.com/archive.zip');
1182
- test.done();
1183
- };
1184
-
1185
- module.exports['Tools: parseBodystructure with message/rfc822'] = test => {
1186
- // message/rfc822 has special handling with envelope and nested bodystructure
1187
- let nestedBody = [
1188
- { value: 'TEXT' },
1189
- { value: 'PLAIN' },
1190
- [{ value: 'CHARSET' }, { value: 'UTF-8' }],
1191
- null,
1192
- null,
1193
- { value: '7BIT' },
1194
- { value: '500' },
1195
- { value: '20' } // line count for text
1196
- ];
1197
-
1198
- let envelope = [
1199
- { value: 'Mon, 15 Jun 2023 10:00:00 +0000' }, // date
1200
- { value: 'Nested Subject' }, // subject
1201
- [[null, null, { value: 'sender' }, { value: 'example.com' }]], // from
1202
- [],
1203
- [],
1204
- [],
1205
- [],
1206
- [], // sender, reply-to, to, cc, bcc
1207
- null, // in-reply-to
1208
- { value: '<nested@example.com>' } // message-id
1209
- ];
1210
-
1211
- let entry = [
1212
- { value: 'MESSAGE' },
1213
- { value: 'RFC822' },
1214
- null, // params
1215
- null, // id
1216
- null, // description
1217
- { value: '7BIT' }, // encoding
1218
- { value: '10000' }, // size
1219
- envelope, // envelope
1220
- nestedBody, // nested bodystructure
1221
- { value: '100' }, // line count
1222
- null, // md5
1223
- null // disposition
1224
- ];
1225
-
1226
- let result = tools.parseBodystructure(entry);
1227
- test.equal(result.type, 'message/rfc822');
1228
- test.equal(result.size, 10000);
1229
- test.equal(result.lineCount, 100);
1230
- test.ok(result.envelope);
1231
- test.equal(result.envelope.subject, 'Nested Subject');
1232
- test.ok(result.childNodes);
1233
- test.equal(result.childNodes.length, 1);
1234
- test.equal(result.childNodes[0].type, 'text/plain');
1235
- test.done();
1236
- };
1237
-
1238
- // ============================================
1239
- // getDecoder tests
1240
- // ============================================
1241
-
1242
- module.exports['Tools: getDecoder with standard charset'] = test => {
1243
- let decoder = tools.getDecoder('utf-8');
1244
- test.ok(decoder);
1245
- test.ok(typeof decoder.write === 'function');
1246
- test.done();
1247
- };
1248
-
1249
- module.exports['Tools: getDecoder with Japanese charset'] = test => {
1250
- let decoder = tools.getDecoder('iso-2022-jp');
1251
- test.ok(decoder);
1252
- test.equal(decoder.constructor.name, 'JPDecoder');
1253
- test.done();
1254
- };
1255
-
1256
- module.exports['Tools: getDecoder with null/undefined'] = test => {
1257
- let decoder = tools.getDecoder(null);
1258
- test.ok(decoder);
1259
- test.done();
1260
- };
1261
-
1262
- // ============================================
1263
- // AuthenticationFailure tests
1264
- // ============================================
1265
-
1266
- module.exports['Tools: AuthenticationFailure error class'] = test => {
1267
- let error = new tools.AuthenticationFailure('Auth failed');
1268
- test.ok(error instanceof Error);
1269
- test.equal(error.authenticationFailed, true);
1270
- test.equal(error.message, 'Auth failed');
1271
- test.done();
1272
- };
1273
-
1274
- // ============================================
1275
- // enhanceCommandError tests
1276
- // ============================================
1277
-
1278
- module.exports['Tools: enhanceCommandError sets serverResponseCode'] = async test => {
1279
- let err = new Error('Command failed');
1280
- err.response = {
1281
- tag: '*',
1282
- command: 'NO',
1283
- attributes: [
1284
- {
1285
- type: 'SECTION',
1286
- section: [{ type: 'ATOM', value: 'NONEXISTENT' }]
1287
- },
1288
- { type: 'ATOM', value: 'Mailbox' },
1289
- { type: 'ATOM', value: 'not' },
1290
- { type: 'ATOM', value: 'found' }
1291
- ]
1292
- };
1293
- let result = await tools.enhanceCommandError(err);
1294
- test.equal(result.serverResponseCode, 'NONEXISTENT');
1295
- test.equal(typeof result.response, 'string');
1296
- test.done();
1297
- };
1298
-
1299
- module.exports['Tools: enhanceCommandError with no status code'] = async test => {
1300
- let err = new Error('Command failed');
1301
- err.response = { tag: '*', command: 'NO' };
1302
- let result = await tools.enhanceCommandError(err);
1303
- test.ok(!result.serverResponseCode);
1304
- test.done();
1305
- };
1306
-
1307
- module.exports['Tools: enhanceCommandError with null response'] = async test => {
1308
- let err = new Error('Command failed');
1309
- err.response = null;
1310
- let result = await tools.enhanceCommandError(err);
1311
- test.equal(result.response, false);
1312
- test.done();
1313
- };
1314
-
1315
- // ============================================
1316
- // getDecoder additional tests
1317
- // ============================================
1318
-
1319
- module.exports['Tools: getDecoder with eucjp charset'] = test => {
1320
- let decoder = tools.getDecoder('eucjp');
1321
- test.ok(decoder);
1322
- test.equal(decoder.constructor.name, 'JPDecoder');
1323
- test.done();
1324
- };
1325
-
1326
- module.exports['Tools: getDecoder with euc-jp (hyphenated) returns JPDecoder'] = test => {
1327
- let decoder = tools.getDecoder('euc-jp');
1328
- test.ok(decoder);
1329
- test.equal(decoder.constructor.name, 'JPDecoder');
1330
- test.done();
1331
- };
1332
-
1333
- module.exports['Tools: getDecoder with jis charset'] = test => {
1334
- let decoder = tools.getDecoder('jis');
1335
- test.ok(decoder);
1336
- test.equal(decoder.constructor.name, 'JPDecoder');
1337
- test.done();
1338
- };
1339
-
1340
- module.exports['Tools: getDecoder with windows-1252 returns iconv stream'] = test => {
1341
- let decoder = tools.getDecoder('windows-1252');
1342
- test.ok(decoder);
1343
- test.notEqual(decoder.constructor.name, 'JPDecoder');
1344
- test.ok(typeof decoder.write === 'function');
1345
- test.done();
1346
- };
1347
-
1348
- module.exports['Tools: getDecoder with no arg defaults to ascii'] = test => {
1349
- let decoder = tools.getDecoder();
1350
- test.ok(decoder);
1351
- test.ok(typeof decoder.write === 'function');
1352
- test.done();
1353
- };
1354
-
1355
- // ============================================
1356
- // getColorFlags tests
1357
- // ============================================
1358
-
1359
- module.exports['Tools: getColorFlags returns non-null for all valid colors'] = test => {
1360
- let colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];
1361
- for (let color of colors) {
1362
- let result = tools.getColorFlags(color);
1363
- test.ok(result, `getColorFlags('${color}') should not be null`);
1364
- test.ok(Array.isArray(result.add), `${color} should have add array`);
1365
- test.ok(Array.isArray(result.remove), `${color} should have remove array`);
1366
- }
1367
- test.done();
1368
- };
1369
-
1370
- module.exports['Tools: getColorFlags red has Flagged but no MailFlagBit set'] = test => {
1371
- let result = tools.getColorFlags('red');
1372
- // red = index 0, all bits 0 — adds \\Flagged, removes all MailFlagBit flags
1373
- test.ok(result.add.includes('\\Flagged'));
1374
- test.ok(result.remove.includes('$MailFlagBit0'));
1375
- test.ok(result.remove.includes('$MailFlagBit1'));
1376
- test.ok(result.remove.includes('$MailFlagBit2'));
1377
- test.done();
1378
- };
1379
-
1380
- module.exports['Tools: getColorFlags orange has MailFlagBit0 set'] = test => {
1381
- let result = tools.getColorFlags('orange');
1382
- // orange = index 1, bit 0 set
1383
- test.ok(result.add.includes('\\Flagged'));
1384
- test.ok(result.add.includes('$MailFlagBit0'));
1385
- test.ok(result.remove.includes('$MailFlagBit1'));
1386
- test.ok(result.remove.includes('$MailFlagBit2'));
1387
- test.done();
1388
- };
1389
-
1390
- module.exports['Tools: getColorFlags yellow has MailFlagBit1 set'] = test => {
1391
- let result = tools.getColorFlags('yellow');
1392
- // yellow = index 2, bit 1 set
1393
- test.ok(result.add.includes('\\Flagged'));
1394
- test.ok(result.add.includes('$MailFlagBit1'));
1395
- test.ok(result.remove.includes('$MailFlagBit0'));
1396
- test.ok(result.remove.includes('$MailFlagBit2'));
1397
- test.done();
1398
- };
1399
-
1400
- module.exports['Tools: getColorFlags returns null for invalid color'] = test => {
1401
- test.equal(tools.getColorFlags('invalid'), null);
1402
- test.equal(tools.getColorFlags('pink'), null);
1403
- test.done();
1404
- };
1405
-
1406
- module.exports['Tools: getColorFlags with null returns result not null'] = test => {
1407
- // null input: colorCode becomes null, which is not < 0, so it falls through
1408
- let result = tools.getColorFlags(null);
1409
- test.ok(result);
1410
- test.ok(result.remove.includes('\\Flagged'));
1411
- test.done();
1412
- };
1413
-
1414
- // ============================================
1415
- // formatMessageResponse: OBJECTID NIL handling (RFC 8474)
1416
- // ============================================
1417
-
1418
- module.exports['Tools: formatMessageResponse handles THREADID NIL'] = async test => {
1419
- // RFC 8474 allows `THREADID NIL` when the server has no thread relation to
1420
- // report (e.g. Strato). The NIL token is parsed as null and must not crash.
1421
- let untagged = await parser('* 1 FETCH (UID 1 EMAILID (E1) THREADID NIL FLAGS (\\Seen) MODSEQ (4312))');
1422
- let result = await tools.formatMessageResponse(untagged, {});
1423
- test.equal(result.seq, 1);
1424
- test.equal(result.uid, 1);
1425
- test.equal(result.emailId, 'E1');
1426
- test.equal(result.threadId, undefined);
1427
- test.ok(result.flags.has('\\Seen'));
1428
- test.equal(result.modseq, 4312n);
1429
- test.done();
1430
- };
1431
-
1432
- module.exports['Tools: formatMessageResponse handles normal THREADID'] = async test => {
1433
- let untagged = await parser('* 2 FETCH (THREADID (T9999) EMAILID (E2))');
1434
- let result = await tools.formatMessageResponse(untagged, {});
1435
- test.equal(result.threadId, 'T9999');
1436
- test.equal(result.emailId, 'E2');
1437
- test.done();
1438
- };
1439
-
1440
- // ============================================
1441
- // formatMessageResponse: BINARY vs BODY part tracking (RFC 3516 / RFC 9051)
1442
- // ============================================
1443
-
1444
- module.exports['Tools: formatMessageResponse records which parts arrived via BINARY'] = async test => {
1445
- // Parts answered as BINARY[...] arrive with the content-transfer-encoding
1446
- // already decoded by the server; consumers (download/downloadMany) use the
1447
- // binaryParts set to skip their own decoder for exactly those parts
1448
- let untagged = await parser('* 1 FETCH (UID 7 BINARY[1] {4}\r\n BODY[2] {4}\r\n)', {
1449
- literals: [Buffer.from('AAAA'), Buffer.from('BBBB')]
1450
- });
1451
- let result = await tools.formatMessageResponse(untagged, {});
1452
- test.equal(result.bodyParts.get('1').toString(), 'AAAA');
1453
- test.equal(result.bodyParts.get('2').toString(), 'BBBB');
1454
- test.ok(result.binaryParts, 'binaryParts set should exist when a BINARY part arrived');
1455
- test.ok(result.binaryParts.has('1'), 'BINARY-answered part is recorded');
1456
- test.ok(!result.binaryParts.has('2'), 'BODY-answered part is not recorded');
1457
- test.done();
1458
- };
1459
-
1460
- module.exports['Tools: formatMessageResponse leaves binaryParts unset for plain BODY fetches'] = async test => {
1461
- let untagged = await parser('* 1 FETCH (UID 8 BODY[1] {4}\r\n)', { literals: [Buffer.from('CCCC')] });
1462
- let result = await tools.formatMessageResponse(untagged, {});
1463
- test.equal(result.bodyParts.get('1').toString(), 'CCCC');
1464
- test.equal(result.binaryParts, undefined);
1465
- test.done();
1466
- };
1467
-
1468
- // ============================================
1469
- // formatMessageResponse: non-ASCII mailbox path normalization for stable id
1470
- // ============================================
1471
-
1472
- module.exports['Tools: formatMessageResponse normalizes non-ASCII mailbox path for id'] = async test => {
1473
- // No EMAILID, so the message id falls back to an md5 over [path, uidValidity, uid].
1474
- // The mailbox path is non-ASCII and must be modified-UTF-7 normalized before hashing.
1475
- // (A previous bogus regex /[0x80-0xff]/ never matched non-ASCII, skipping normalization.)
1476
- let untagged = await parser('* 1 FETCH (UID 5 FLAGS (\\Seen))');
1477
- let mailbox = { path: '日本語', uidValidity: 123n };
1478
- let result = await tools.formatMessageResponse(untagged, mailbox);
1479
-
1480
- let encodedPath = iconv.encode('日本語', 'utf-7-imap').toString();
1481
- let expectedId = crypto.createHash('md5').update([encodedPath, '123', '5'].join(':')).digest('hex');
1482
- let rawId = crypto.createHash('md5').update(['日本語', '123', '5'].join(':')).digest('hex');
1483
-
1484
- test.equal(result.id, expectedId, 'id must hash the UTF-7 normalized path');
1485
- test.notEqual(result.id, rawId, 'id must not hash the raw non-ASCII path');
1486
- test.done();
1487
- };
1488
-
1489
- module.exports['Tools: formatMessageResponse parses the full attribute set'] = async test => {
1490
- let untagged = await parser(
1491
- '* 3 FETCH (UID 100 RFC822.SIZE 5000 FLAGS (\\Seen \\Flagged) MODSEQ (12345) ' +
1492
- 'X-GM-MSGID 999 X-GM-THRID 888 X-GM-LABELS (\\Important Work) ' +
1493
- 'INTERNALDATE "12-Jan-2020 10:00:00 +0000" BODY[1] {3}\r\n BODY[HEADER] {2}\r\n)',
1494
- { literals: [Buffer.from('abc'), Buffer.from('hi')] }
1495
- );
1496
- let mailbox = { path: 'INBOX', uidValidity: 1n }; // no uidNext/highestModseq -> exercise the bump branches
1497
- let r = await tools.formatMessageResponse(untagged, mailbox);
1498
- test.equal(r.uid, 100);
1499
- test.equal(r.size, 5000);
1500
- test.ok(r.flags.has('\\Seen'));
1501
- test.equal(r.modseq, 12345n);
1502
- test.equal(r.emailId, '999'); // X-GM-MSGID
1503
- test.equal(r.threadId, '888'); // X-GM-THRID
1504
- test.ok(r.labels.has('Work'));
1505
- test.ok(r.internalDate instanceof Date);
1506
- test.ok(Buffer.isBuffer(r.headers));
1507
- test.ok(r.bodyParts.get('1'));
1508
- // mailbox estimates bumped from the FETCH data
1509
- test.equal(mailbox.uidNext, 101);
1510
- test.equal(mailbox.highestModseq, 12345n);
1511
- test.done();
1512
- };
1513
-
1514
- module.exports['Tools: formatMessageResponse parses envelope and bodystructure'] = async test => {
1515
- let untagged = await parser(
1516
- '* 1 FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700" "Subject" ((NIL NIL "a" "b.com")) NIL NIL NIL NIL NIL NIL "<id@x>") ' +
1517
- 'BODYSTRUCTURE ("TEXT" "PLAIN" ("CHARSET" "utf-8") NIL NIL "7BIT" 12 0 NIL NIL NIL))'
1518
- );
1519
- let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
1520
- test.ok(r.envelope);
1521
- test.equal(r.envelope.subject, 'Subject');
1522
- test.ok(r.bodyStructure);
1523
- test.equal(r.bodyStructure.type, 'text/plain');
1524
- test.done();
1525
- };
1526
-
1527
- module.exports['Tools: formatMessageResponse source from BODY[] and BINARY[] literals'] = async test => {
1528
- let untagged = await parser('* 1 FETCH (BODY[] {5}\r\n)', { literals: [Buffer.from('HELLO')] });
1529
- let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
1530
- test.ok(Buffer.isBuffer(r.source));
1531
- test.equal(r.source.toString(), 'HELLO');
1532
- test.done();
1533
- };
1534
-
1535
- module.exports['Tools: formatMessageResponse keeps invalid INTERNALDATE as raw string'] = async test => {
1536
- let untagged = await parser('* 1 FETCH (INTERNALDATE "not a date")');
1537
- let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
1538
- test.equal(r.internalDate, 'not a date');
1539
- test.done();
1540
- };
1541
-
1542
- module.exports['Tools: parseEnvelope parses every field'] = async test => {
1543
- let untagged = await parser(
1544
- '* 1 FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700" "Hello" ' +
1545
- '((NIL NIL "from" "x.com")) ((NIL NIL "sender" "x.com")) ((NIL NIL "reply" "x.com")) ' +
1546
- '((NIL NIL "to" "x.com")) ((NIL NIL "cc" "x.com")) ((NIL NIL "bcc" "x.com")) ' +
1547
- '"<inreplyto@x>" "<msgid@x>"))'
1548
- );
1549
- let env = tools.parseEnvelope(untagged.attributes[1][1]);
1550
- test.ok(env.date instanceof Date);
1551
- test.equal(env.subject, 'Hello');
1552
- test.equal(env.from[0].address, 'from@x.com');
1553
- test.equal(env.sender[0].address, 'sender@x.com');
1554
- test.equal(env.replyTo[0].address, 'reply@x.com');
1555
- test.equal(env.to[0].address, 'to@x.com');
1556
- test.equal(env.cc[0].address, 'cc@x.com');
1557
- test.equal(env.bcc[0].address, 'bcc@x.com');
1558
- test.equal(env.inReplyTo, '<inreplyto@x>');
1559
- test.equal(env.messageId, '<msgid@x>');
1560
- test.done();
1561
- };
1562
-
1563
- module.exports['Tools: parseEnvelope keeps invalid date as string'] = async test => {
1564
- let untagged = await parser('* 1 FETCH (ENVELOPE ("not a date" "Subj" NIL NIL NIL NIL NIL NIL NIL NIL))');
1565
- let env = tools.parseEnvelope(untagged.attributes[1][1]);
1566
- test.equal(env.date, 'not a date');
1567
- test.done();
1568
- };
1569
-
1570
- module.exports['Tools: parseBodystructure parses all single-part extension fields'] = async test => {
1571
- let bs =
1572
- '("TEXT" "PLAIN" ("CHARSET" "utf-8") "<cid@x>" "a description" "BASE64" 100 5 ' +
1573
- '"d41d8cd9" ("attachment" ("filename" "f.txt")) ("en" "de") "http://loc/")';
1574
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1575
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1576
- test.equal(node.type, 'text/plain');
1577
- test.equal(node.id, '<cid@x>');
1578
- test.equal(node.description, 'a description');
1579
- test.equal(node.encoding, 'base64');
1580
- test.equal(node.size, 100);
1581
- test.equal(node.lineCount, 5);
1582
- test.equal(node.md5, 'd41d8cd9');
1583
- test.equal(node.disposition, 'attachment');
1584
- test.equal(node.dispositionParameters.filename, 'f.txt');
1585
- test.deepEqual(node.language, ['en', 'de']);
1586
- test.done();
1587
- };
1588
-
1589
- module.exports['Tools: parseBodystructure parses message/rfc822 with envelope and child'] = async test => {
1590
- let bs =
1591
- '("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 200 ' +
1592
- '("date" "subj" NIL NIL NIL NIL NIL NIL "<reply>" "<msgid>") ' +
1593
- '("TEXT" "PLAIN" NIL NIL NIL "7BIT" 10 1) 3)';
1594
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1595
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1596
- test.equal(node.type, 'message/rfc822');
1597
- test.ok(node.envelope);
1598
- test.equal(node.childNodes.length, 1);
1599
- test.equal(node.lineCount, 3);
1600
- test.done();
1601
- };
1602
-
1603
- module.exports['Tools: parseBodystructure parses multipart with params and disposition'] = async test => {
1604
- let bs = '(("TEXT" "PLAIN" NIL NIL NIL "7BIT" 10 1)("TEXT" "HTML" NIL NIL NIL "7BIT" 20 2) "ALTERNATIVE" ("BOUNDARY" "xyz") ("inline" NIL) ("en"))';
1605
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1606
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1607
- test.equal(node.type, 'multipart/alternative');
1608
- test.equal(node.childNodes.length, 2);
1609
- test.equal(node.parameters.boundary, 'xyz');
1610
- test.equal(node.disposition, 'inline');
1611
- test.done();
1612
- };
1613
-
1614
- module.exports['Tools: formatMessageResponse handles NIL, Buffer and unknown keys'] = async test => {
1615
- let untagged = await parser('* 1 FETCH (UID NIL RFC822.SIZE NIL X-GM-MSGID {3}\r\n BODY[] NIL FOOBAR 123)', {
1616
- literals: [Buffer.from('999')]
1617
- });
1618
- let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
1619
- test.equal(r.uid, undefined); // a NIL UID is left unset rather than reported as UID 0
1620
- test.equal(r.size, 0); // size keeps its 0 default
1621
-
1622
- test.equal(r.emailId, '999'); // Buffer literal value -> getString stringifies it
1623
- test.equal(r.source, false); // BODY[] NIL -> getBuffer returns false
1624
- test.done();
1625
- };
1626
-
1627
- module.exports['Tools: getFolderTree merges duplicate folder entries'] = test => {
1628
- let tree = tools.getFolderTree([
1629
- { name: 'Parent', path: 'Parent', parent: [], flags: new Set(['\\HasChildren']), delimiter: '/' },
1630
- { name: 'Parent', path: 'Parent', parent: [], flags: new Set(['\\Noselect', '\\HasChildren']), specialUse: '\\Sent', delimiter: '/' }
1631
- ]);
1632
- test.equal(tree.folders.length, 1);
1633
- test.equal(tree.folders[0].disabled, true);
1634
- test.equal(tree.folders[0].specialUse, '\\Sent');
1635
- test.done();
1636
- };
1637
-
1638
- module.exports['Tools: parseBodystructure handles minimal NIL fields'] = async test => {
1639
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ("APPLICATION" "OCTET-STREAM" NIL NIL NIL NIL NIL))');
1640
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1641
- test.equal(node.type, 'application/octet-stream');
1642
- test.equal(node.id, undefined);
1643
- test.equal(node.encoding, undefined);
1644
- test.done();
1645
- };
1646
-
1647
- module.exports['Tools: parseBodystructure decodes RFC 2231 charset continuation params'] = async test => {
1648
- let bs = '("TEXT" "PLAIN" ("name*0*" "utf-8\'\'%E2%82%AC abc" "name*1" "def") NIL NIL "7BIT" 10 1)';
1649
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1650
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1651
- test.equal(node.parameters.name, '€ abcdef');
1652
- test.done();
1653
- };
1654
-
1655
- module.exports['Tools: parseBodystructure handles empty-string extension fields'] = async test => {
1656
- // Empty-string values exercise the `|| ''` / `|| 0` fallbacks in each field.
1657
- let bs = '("TEXT" "PLAIN" ("CHARSET" "") "" "" "" 0 0 "" ("" ("" "")) ("") "")';
1658
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1659
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1660
- test.equal(node.id, '');
1661
- test.equal(node.description, '');
1662
- test.equal(node.size, 0);
1663
- test.equal(node.md5, '');
1664
- test.deepEqual(node.language, ['']);
1665
- test.done();
1666
- };
1667
-
1668
- module.exports['Tools: formatMessageResponse with no data list returns just seq'] = async test => {
1669
- let untagged = await parser('* 5 FETCH');
1670
- let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
1671
- test.equal(r.seq, 5);
1672
- test.done();
1673
- };
1674
-
1675
- module.exports['Tools: getFolderTree adds folders array when existing entry gains HasChildren'] = test => {
1676
- let tree = tools.getFolderTree([
1677
- { name: 'P', path: 'P', parent: [], flags: new Set([]), delimiter: '/' },
1678
- { name: 'P', path: 'P', parent: [], flags: new Set(['\\HasChildren']), delimiter: '/' }
1679
- ]);
1680
- test.ok(Array.isArray(tree.folders[0].folders), 'folders array created on merge');
1681
- test.done();
1682
- };
1683
-
1684
- module.exports['Tools: normalizePath joins array with empty delimiter fallback'] = test => {
1685
- // namespace present but without a delimiter -> the (... || '') fallback joins directly
1686
- let connection = { namespace: { prefix: '' } };
1687
- test.equal(tools.normalizePath(connection, ['a', 'b']), 'ab');
1688
- test.done();
1689
- };
1690
-
1691
- module.exports['Tools: updateCapabilities defaults non-numeric APPENDLIMIT to 0'] = test => {
1692
- let caps = tools.updateCapabilities([{ value: 'APPENDLIMIT=abc' }]);
1693
- test.equal(caps.get('APPENDLIMIT'), 0);
1694
- test.done();
1695
- };
1696
-
1697
- module.exports['Tools: formatMessageResponse skips non-string label entries'] = async test => {
1698
- let untagged = await parser('* 1 FETCH (X-GM-LABELS (\\Important NIL))');
1699
- let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
1700
- test.deepEqual([...r.labels], ['\\Important']); // NIL entry filtered out
1701
- test.done();
1702
- };
1703
-
1704
- module.exports['Tools: formatMessageResponse does not lower highestModseq'] = async test => {
1705
- let mailbox = { path: 'INBOX', highestModseq: 99999n };
1706
- let untagged = await parser('* 1 FETCH (MODSEQ (5) FLAGS (\\Seen))');
1707
- await tools.formatMessageResponse(untagged, mailbox);
1708
- test.equal(mailbox.highestModseq, 99999n); // unchanged: incoming modseq is lower
1709
- test.done();
1710
- };
1711
-
1712
- module.exports['Tools: parseBodystructure multipart with NIL subtype'] = async test => {
1713
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE (("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1) NIL))');
1714
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1715
- test.equal(node.type, 'multipart/');
1716
- test.done();
1717
- };
1718
-
1719
- module.exports['Tools: parseBodystructure content type with NIL type/subtype'] = async test => {
1720
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE (NIL NIL NIL NIL NIL "7BIT" 1))');
1721
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1722
- test.equal(node.type, '/');
1723
- test.done();
1724
- };
1725
-
1726
- module.exports['Tools: parseBodystructure message/rfc822 with NIL envelope/linecount'] = async test => {
1727
- let bs = '("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 100 NIL ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1) NIL NIL (NIL) NIL)';
1728
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1729
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1730
- test.equal(node.type, 'message/rfc822');
1731
- test.done();
1732
- };
1733
-
1734
- module.exports['Tools: parseBodystructure text with NIL language/location entries'] = async test => {
1735
- let bs = '("TEXT" "PLAIN" NIL NIL NIL "7BIT" 100 NIL "md5" ("inline" NIL) (NIL) NIL)';
1736
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1737
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1738
- test.deepEqual(node.language, ['']);
1739
- test.done();
1740
- };
1741
-
1742
- module.exports['Tools: parseBodystructure empty-string size/linecount/language/location'] = async test => {
1743
- let bs = '("TEXT" "PLAIN" NIL NIL NIL "7BIT" "" "" "" ("inline" NIL) ("") "")';
1744
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1745
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1746
- test.equal(node.size, 0);
1747
- test.equal(node.lineCount, 0);
1748
- test.deepEqual(node.language, ['']);
1749
- test.done();
1750
- };
1751
-
1752
- module.exports['Tools: parseBodystructure message/rfc822 empty-string linecount'] = async test => {
1753
- let bs = '("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 100 ("d" "s" NIL NIL NIL NIL NIL NIL NIL NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1) "")';
1754
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1755
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1756
- test.equal(node.type, 'message/rfc822');
1757
- test.equal(node.lineCount, 0);
1758
- test.done();
1759
- };
1760
-
1761
- module.exports['Tools: parseBodystructure decodes RFC 2231 with empty charset and special chars'] = async test => {
1762
- // Empty charset before the first quote defaults to utf-8; '=' / '?' in the value
1763
- // exercise the 2-char hex escape branch; the empty *1* continuation segment too.
1764
- // value includes '=' / '?' (2-char hex escapes), a space (-> '_') and a TAB
1765
- // (a <0x10 control char -> single-hex-digit escape needing a '0' pad).
1766
- let bs = '("TEXT" "PLAIN" ("name*0*" "\'\'a=b?c d\te" "name*1*" "") NIL NIL "7BIT" 1 1)';
1767
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1768
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1769
- test.ok(node.parameters.name.includes('a=b?c'));
1770
- test.done();
1771
- };
1772
-
1773
- module.exports['Tools: parseBodystructure parses empty-string body location'] = async test => {
1774
- // A trailing element after location makes the (i < length-1) guard pass so the
1775
- // empty-string location value goes through the `|| ''` fallback.
1776
- let bs = '("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1 NIL NIL NIL "" NIL)';
1777
- let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
1778
- let node = tools.parseBodystructure(untagged.attributes[1][1]);
1779
- test.equal(node.location, '');
1780
- test.done();
1781
- };
1782
-
1783
- module.exports['Tools: expandRange skips descending range to non-numeric second bound'] = test => {
1784
- // Endpoints that are not valid nz-numbers make the whole entry invalid -
1785
- // the pre-hardening behavior expanded '5:abc' down to 0
1786
- test.deepEqual(tools.expandRange('5:abc'), []);
1787
- test.done();
1788
- };
1789
-
1790
- module.exports['Tools: expandRange skips non-numeric entries but keeps valid ones'] = test => {
1791
- test.deepEqual(tools.expandRange('abc,:5,1:3'), [1, 2, 3]);
1792
- test.done();
1793
- };
1794
-
1795
- module.exports['Tools: expandRange handles descending ranges'] = test => {
1796
- test.deepEqual(tools.expandRange('5:3'), [5, 4, 3]);
1797
- test.done();
1798
- };
1799
-
1800
- module.exports['Tools: encodePath keeps name as-is when iconv encode throws'] = test => {
1801
- // The iconv module object is shared by reference; patch encode to throw so the
1802
- // defensive catch is exercised, then restore.
1803
- let original = iconv.encode;
1804
- iconv.encode = () => {
1805
- throw new Error('encode boom');
1806
- };
1807
- try {
1808
- let connection = { enabled: new Set(), capabilities: new Map() };
1809
- let result = tools.encodePath(connection, 'Tärä');
1810
- test.equal(result, 'Tärä', 'falls back to the raw path');
1811
- } finally {
1812
- iconv.encode = original;
1813
- }
1814
- test.done();
1815
- };
1816
-
1817
- module.exports['Tools: decodePath keeps name as-is when iconv decode throws'] = test => {
1818
- let original = iconv.decode;
1819
- iconv.decode = () => {
1820
- throw new Error('decode boom');
1821
- };
1822
- try {
1823
- let connection = { enabled: new Set(), capabilities: new Map() };
1824
- let result = tools.decodePath(connection, 'Inbox&AOk-');
1825
- test.equal(result, 'Inbox&AOk-');
1826
- } finally {
1827
- iconv.decode = original;
1828
- }
1829
- test.done();
1830
- };
1831
-
1832
- module.exports['Tools: formatMessageResponse id falls back when iconv encode throws'] = async test => {
1833
- let original = iconv.encode;
1834
- iconv.encode = () => {
1835
- throw new Error('encode boom');
1836
- };
1837
- try {
1838
- let untagged = await parser('* 1 FETCH (UID 5 FLAGS (\\Seen))');
1839
- // non-ASCII path triggers the encode branch; the throw is swallowed
1840
- let r = await tools.formatMessageResponse(untagged, { path: 'Tärä', uidValidity: 1n });
1841
- test.ok(r.id, 'an id is still produced');
1842
- } finally {
1843
- iconv.encode = original;
1844
- }
1845
- test.done();
1846
- };
1847
-
1848
- // ============================================
1849
- // packMessageRange
1850
- // ============================================
1851
-
1852
- module.exports['Tools: packMessageRange packs contiguous and gapped ranges'] = test => {
1853
- test.equal(tools.packMessageRange([1, 2, 3, 5, 7, 8]), '1:3,5,7:8');
1854
- test.equal(tools.packMessageRange(7), '7');
1855
- test.equal(tools.packMessageRange([]), '');
1856
- test.done();
1857
- };
1858
-
1859
- module.exports['Tools: packMessageRange dedupes duplicate values'] = test => {
1860
- // Duplicates must not produce overlapping/non-canonical tokens like "1,1:3".
1861
- test.equal(tools.packMessageRange([1, 1, 2, 3]), '1:3');
1862
- test.equal(tools.packMessageRange([3, 1, 2, 2, 5]), '1:3,5');
1863
- test.equal(tools.packMessageRange([5, 5, 5]), '5');
1864
- test.done();
1865
- };
1866
-
1867
- // ============================================
1868
- // Security regression tests: hostile server input
1869
- // ============================================
1870
-
1871
- module.exports['Tools: getStructuredParams drops __proto__ continuation parameters'] = test => {
1872
- // A parameter named "__proto__*0*" groups under "__proto__": without a guard the
1873
- // grouping wrote attacker-controlled values onto Object.prototype (and then threw)
1874
- let result = tools.getStructuredParams([{ value: '__proto__*0*' }, { value: "utf-8''polluted" }]);
1875
- test.deepEqual(result, {});
1876
- test.equal({}.charset, undefined, 'Object.prototype must stay clean');
1877
- test.done();
1878
- };
1879
-
1880
- module.exports['Tools: getStructuredParams drops a plain __proto__ parameter'] = test => {
1881
- // A plain "__proto__" parameter never reaches the output and never touches the prototype.
1882
- // The string value alone cannot pollute (the __proto__ setter ignores primitives) - the
1883
- // object-valued continuation form above is the case the live guard covers - so this
1884
- // pins the output contract: the parameter is dropped, the rest of the list survives.
1885
- let result = tools.getStructuredParams([{ value: '__proto__' }, { value: 'polluted' }, { value: 'a' }, { value: 'b' }]);
1886
- test.deepEqual(result, { a: 'b' });
1887
- test.ok(!Object.prototype.hasOwnProperty.call(result, '__proto__'), 'no own __proto__ property is created');
1888
- test.equal(Object.getPrototypeOf(result), Object.prototype, 'the prototype must stay untouched');
1889
- test.done();
1890
- };
1891
-
1892
- module.exports['Tools: formatMessageResponse ignores a non-numeric MODSEQ'] = async test => {
1893
- // BigInt() throws on garbage, and the throw used to drop the whole message
1894
- // from the result set - a malformed MODSEQ must be skipped instead
1895
- let untagged = await parser('* 1 FETCH (UID 1 MODSEQ (abc) FLAGS (\\Seen))');
1896
- let result = await tools.formatMessageResponse(untagged, {});
1897
- test.equal(result.uid, 1);
1898
- test.equal(result.modseq, undefined);
1899
- test.ok(result.flags.has('\\Seen'));
1900
-
1901
- // an unparenthesized MODSEQ yields no usable value either
1902
- let untagged2 = await parser('* 1 FETCH (UID 1 MODSEQ 123)');
1903
- let result2 = await tools.formatMessageResponse(untagged2, {});
1904
- test.equal(result2.uid, 1);
1905
- test.equal(result2.modseq, undefined);
1906
- test.done();
1907
- };
1908
-
1909
- module.exports['Tools: parseEnvelope skips NIL address entries'] = test => {
1910
- // A NIL inside an address list used to throw on the dereference, dropping the message
1911
- let entry = [
1912
- null, // date
1913
- null, // subject
1914
- [null, [{ value: 'Name' }, null, { value: 'user' }, { value: 'example.com' }]], // from: NIL entry + valid entry
1915
- [], // sender
1916
- [], // reply-to
1917
- [], // to
1918
- [], // cc
1919
- [], // bcc
1920
- null, // in-reply-to
1921
- null // message-id
1922
- ];
1923
-
1924
- let result = tools.parseEnvelope(entry);
1925
- test.equal(result.from.length, 1);
1926
- test.equal(result.from[0].address, 'user@example.com');
1927
- test.done();
1928
- };
1929
-
1930
- // ============================================
1931
- // Bounded parsing of untrusted numeric values
1932
- // ============================================
1933
-
1934
- module.exports['Tools: expandRange returns nothing for a non-string range'] = test => {
1935
- // untaggedVanished() leaves the sequence set as `false` when the response carries only the
1936
- // (EARLIER) tag, and throwing here would abort the handler for the rest of the response
1937
- test.deepEqual(tools.expandRange(false), []);
1938
- test.deepEqual(tools.expandRange(undefined), []);
1939
- test.deepEqual(tools.expandRange(null), []);
1940
- test.deepEqual(tools.expandRange(123), []);
1941
- test.deepEqual(tools.expandRange(['1:3']), []);
1942
- test.deepEqual(tools.expandRange('1:3'), [1, 2, 3], 'a real range still expands');
1943
- test.done();
1944
- };
1945
-
1946
- module.exports['Tools: isDecimalString accepts only bounded digit runs'] = test => {
1947
- test.equal(tools.isDecimalString('12', 10), true);
1948
- test.equal(tools.isDecimalString('1234567890', 10), true);
1949
- test.equal(tools.isDecimalString('12345678901', 10), false, 'one digit past the bound is rejected');
1950
- test.equal(tools.isDecimalString('', 10), false);
1951
- test.equal(tools.isDecimalString('1e5', 10), false);
1952
- test.equal(tools.isDecimalString(' 12 ', 10), false);
1953
- test.equal(tools.isDecimalString('0x10', 10), false);
1954
- test.equal(tools.isDecimalString('-1', 10), false);
1955
- test.equal(tools.isDecimalString('1.5', 10), false);
1956
- test.equal(tools.isDecimalString(12, 10), false, 'only strings are accepted');
1957
- test.equal(tools.isDecimalString(null, 10), false);
1958
- test.done();
1959
- };
1960
-
1961
- module.exports['Tools: parseBigIntValue rejects everything BigInt would throw on'] = test => {
1962
- // isNaN() passes '1e5', ' 12 ' and 'Infinity'; BigInt() throws on all three, and the throw
1963
- // propagates out of a response handler that was only trying to read one field
1964
- test.equal(tools.parseBigIntValue('9122'), 9122n);
1965
- for (let bad of ['1e5', ' 12 ', 'Infinity', 'none', '', '-1', '1.5', null, undefined, 12, ['1']]) {
1966
- test.equal(tools.parseBigIntValue(bad), false, `${JSON.stringify(bad)} must be rejected`);
1967
- }
1968
- test.done();
1969
- };
1970
-
1971
- module.exports['Tools: parseBigIntValue bounds the digit count before converting'] = test => {
1972
- // BigInt() on a multi-megabyte digit run costs hundreds of milliseconds of non-yielding
1973
- // CPU, and a response line may carry up to maxLineLength digits
1974
- test.equal(tools.parseBigIntValue('9'.repeat(19)), BigInt('9'.repeat(19)), '63-bit values still fit the default bound');
1975
- test.equal(tools.parseBigIntValue('9'.repeat(20)), false);
1976
- test.equal(tools.parseBigIntValue('9'.repeat(400000)), false);
1977
- test.equal(tools.parseBigIntValue('12345678901', tools.MAX_UINT32_DIGITS), false, 'a tighter bound can be requested');
1978
- test.done();
1979
- };
1980
-
1981
- module.exports['Tools: parseUintValue rejects values outside the safe integer range'] = test => {
1982
- test.equal(tools.parseUintValue('1000'), 1000);
1983
- test.equal(tools.parseUintValue('0'), 0);
1984
- test.equal(tools.parseUintValue('9'.repeat(19)), false, 'a value past 2^53-1 is refused, not rounded');
1985
- test.equal(tools.parseUintValue('9'.repeat(400)), false, 'a digit run that would coerce to Infinity is refused');
1986
- for (let bad of ['1e3', '0x10', ' 12 ', '-5', '1.5', '']) {
1987
- test.equal(tools.parseUintValue(bad), false, `${JSON.stringify(bad)} must be rejected`);
1988
- }
1989
- test.done();
1990
- };
1991
-
1992
- module.exports['Tools: expandRange caps the total expansion, not just each range'] = test => {
1993
- // A per-range bound alone is multiplied by an unbounded number of ranges. With the budget
1994
- // already spent inside the second range, the two after it must be abandoned rather than
1995
- // expanded on top of it.
1996
- let result = tools.expandRange('1:16777215,16777215:16777220,1:5,7');
1997
- test.equal(result.length, tools.EXPANDED_RANGE_LIMIT);
1998
- test.equal(result[0], 1);
1999
- // the second range gets the single remaining entry, and the two after it never start
2000
- test.equal(result[result.length - 1], 16777215);
2001
- test.done();
2002
- };
2003
-
2004
- module.exports['Tools: formatMessageResponse drops an unusable sequence number'] = async test => {
2005
- // Number('9'.repeat(400)) is Infinity, and a seq of Infinity indexes into nothing
2006
- let untagged = await parser('* 1 FETCH (UID 5)');
2007
- let ok = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
2008
- test.equal(ok.seq, 1);
2009
-
2010
- let overflowing = await tools.formatMessageResponse({ command: '9'.repeat(400), attributes: [] }, { path: 'INBOX' });
2011
- test.equal(overflowing.seq, undefined);
2012
- test.done();
2013
- };