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,418 +0,0 @@
1
- 'use strict';
2
-
3
- const specialUse = require('../lib/special-use');
4
- const fs = require('fs');
5
-
6
- module.exports['Special Use: flags array'] = test => {
7
- test.ok(Array.isArray(specialUse.flags));
8
- test.ok(specialUse.flags.includes('\\Sent'));
9
- test.ok(specialUse.flags.includes('\\Drafts'));
10
- test.ok(specialUse.flags.includes('\\Trash'));
11
- test.ok(specialUse.flags.includes('\\Archive'));
12
- test.done();
13
- };
14
-
15
- module.exports['Special Use: names object'] = test => {
16
- test.ok(typeof specialUse.names === 'object');
17
- test.ok(specialUse.names['\\Sent']);
18
- test.ok(Array.isArray(specialUse.names['\\Sent']));
19
- test.ok(specialUse.names['\\Sent'].includes('sent'));
20
- test.done();
21
- };
22
-
23
- module.exports['Special Use: Sent folder names'] = test => {
24
- let sentNames = specialUse.names['\\Sent'];
25
- test.ok(sentNames.includes('sent'));
26
- test.ok(sentNames.includes('sent items'));
27
- test.ok(sentNames.includes('sent messages'));
28
- test.done();
29
- };
30
-
31
- module.exports['Special Use: Drafts folder names'] = test => {
32
- let draftsNames = specialUse.names['\\Drafts'];
33
- test.ok(draftsNames.includes('drafts'));
34
- test.done();
35
- };
36
-
37
- module.exports['Special Use: Trash folder names'] = test => {
38
- let trashNames = specialUse.names['\\Trash'];
39
- test.ok(trashNames.includes('trash'));
40
- test.ok(trashNames.includes('deleted items'));
41
- test.ok(trashNames.includes('deleted messages'));
42
- test.done();
43
- };
44
-
45
- module.exports['Special Use: Junk folder names'] = test => {
46
- let junkNames = specialUse.names['\\Junk'];
47
- test.ok(junkNames.includes('spam'));
48
- test.ok(junkNames.includes('junk'));
49
- test.done();
50
- };
51
-
52
- // ============================================
53
- // specialUse() function branch tests
54
- // ============================================
55
-
56
- module.exports['Special Use: specialUse returns extension flag when extension enabled and flag found'] = test => {
57
- const result = specialUse.specialUse(true, { flags: new Set(['\\Sent']), name: 'Foo' });
58
- test.equal(result.flag, '\\Sent');
59
- test.equal(result.source, 'extension');
60
- test.done();
61
- };
62
-
63
- module.exports['Special Use: specialUse falls back to name when extension enabled but no flag'] = test => {
64
- const result = specialUse.specialUse(true, { flags: new Set(), name: 'Sent' });
65
- test.equal(result.flag, '\\Sent');
66
- test.equal(result.source, 'name');
67
- test.done();
68
- };
69
-
70
- module.exports['Special Use: specialUse matches by name when extension disabled'] = test => {
71
- const result = specialUse.specialUse(false, { flags: new Set(), name: 'Drafts' });
72
- test.equal(result.flag, '\\Drafts');
73
- test.equal(result.source, 'name');
74
- test.done();
75
- };
76
-
77
- module.exports['Special Use: specialUse returns null flag when no match'] = test => {
78
- const result = specialUse.specialUse(false, { flags: new Set(), name: 'CustomFolder' });
79
- test.equal(result.flag, null);
80
- test.equal(result.source, undefined);
81
- test.done();
82
- };
83
-
84
- // ============================================
85
- // Localized folder name detection
86
- // ============================================
87
-
88
- // Helper: resolve a folder name the way a server without SPECIAL-USE would be handled.
89
- const byName = name => specialUse.specialUse(false, { flags: new Set(), name }).flag;
90
-
91
- // Regression: Exchange/Outlook does not advertise SPECIAL-USE, so a Russian-locale
92
- // mailbox has to resolve purely by folder name. Junk and Archive used to fall through.
93
- module.exports['Special Use: Russian Outlook mailbox resolves every special-use folder'] = test => {
94
- test.equal(byName('Отправленные'), '\\Sent');
95
- test.equal(byName('Черновики'), '\\Drafts');
96
- test.equal(byName('Удаленные'), '\\Trash');
97
- test.equal(byName('Нежелательная почта'), '\\Junk');
98
- test.equal(byName('Архив'), '\\Archive');
99
- test.done();
100
- };
101
-
102
- // Non-mail folders exposed over IMAP by Exchange must stay unclassified.
103
- module.exports['Special Use: Russian calendar and contacts folders stay unmatched'] = test => {
104
- test.equal(byName('Календарь'), null);
105
- test.equal(byName('Контакты'), null);
106
- test.done();
107
- };
108
-
109
- // Russian webmail (Yandex, Mail.ru, Rambler) uses different words than Outlook.
110
- module.exports['Special Use: Russian webmail folder names'] = test => {
111
- test.equal(byName('Корзина'), '\\Trash');
112
- test.equal(byName('Удалённые'), '\\Trash'); // spelled with a real "ё"
113
- test.equal(byName('Спам'), '\\Junk');
114
- test.done();
115
- };
116
-
117
- module.exports['Special Use: Archive folder names across locales'] = test => {
118
- test.equal(byName('Archive'), '\\Archive');
119
- test.equal(byName('Архив'), '\\Archive');
120
- test.equal(byName('Архів'), '\\Archive');
121
- test.equal(byName('Archiv'), '\\Archive');
122
- test.equal(byName('Archivo'), '\\Archive');
123
- test.equal(byName('Arquivo'), '\\Archive');
124
- test.equal(byName('Archivio'), '\\Archive');
125
- test.equal(byName('Archief'), '\\Archive');
126
- test.equal(byName('Arkiv'), '\\Archive');
127
- test.equal(byName('Arşiv'), '\\Archive');
128
- test.equal(byName('アーカイブ'), '\\Archive');
129
- test.equal(byName('보관함'), '\\Archive');
130
- test.done();
131
- };
132
-
133
- module.exports['Special Use: Junk folder names across locales'] = test => {
134
- test.equal(byName('Junk Email'), '\\Junk');
135
- test.equal(byName('Нежелательная почта'), '\\Junk');
136
- test.equal(byName('Небажана пошта'), '\\Junk');
137
- test.equal(byName('Ongewenste e-mail'), '\\Junk');
138
- test.equal(byName('Lixo Eletrônico'), '\\Junk');
139
- test.equal(byName('Neželjena pošta'), '\\Junk');
140
- test.equal(byName('迷惑メール'), '\\Junk');
141
- test.equal(byName('정크 메일'), '\\Junk');
142
- test.done();
143
- };
144
-
145
- module.exports['Special Use: Trash folder names across locales'] = test => {
146
- test.equal(byName('Deleted Items'), '\\Trash');
147
- test.equal(byName('Corbeille'), '\\Trash');
148
- test.equal(byName('Papierkorb'), '\\Trash');
149
- test.equal(byName('Papelera'), '\\Trash');
150
- test.equal(byName('Cestino'), '\\Trash');
151
- test.equal(byName('Kosz'), '\\Trash');
152
- test.equal(byName('ゴミ箱'), '\\Trash');
153
- test.equal(byName('휴지통'), '\\Trash');
154
- test.done();
155
- };
156
-
157
- // Matching lowercases the folder name, so non-ASCII scripts must fold too.
158
- module.exports['Special Use: name matching is case insensitive for non-ASCII names'] = test => {
159
- test.equal(byName('НЕЖЕЛАТЕЛЬНАЯ ПОЧТА'), '\\Junk');
160
- test.equal(byName('АРХИВ'), '\\Archive');
161
- test.equal(byName('Корзина'), '\\Trash');
162
- test.done();
163
- };
164
-
165
- // Surrounding whitespace and the LTR mark injected by some clients are stripped.
166
- module.exports['Special Use: strips whitespace and LTR marks before matching'] = test => {
167
- test.equal(byName(' Архив '), '\\Archive');
168
- test.equal(byName('\u200eНежелательная почта'), '\\Junk');
169
- test.done();
170
- };
171
-
172
- // Exchange and Outlook use a two word naming style ("Sent Items") that differs from
173
- // the one word style most webmail uses ("Sent"), and they never advertise SPECIAL-USE,
174
- // so these names have to resolve by name alone.
175
- module.exports['Special Use: Exchange and Outlook localized folder names'] = test => {
176
- test.equal(byName('Sent Items'), '\\Sent');
177
- test.equal(byName('Deleted Items'), '\\Trash');
178
- test.equal(byName('Junk Email'), '\\Junk');
179
- test.equal(byName('Gesendete Elemente'), '\\Sent');
180
- test.equal(byName('Gelöschte Elemente'), '\\Trash');
181
- test.equal(byName('Éléments envoyés'), '\\Sent');
182
- test.equal(byName('Elementos eliminados'), '\\Trash');
183
- test.equal(byName('Posta eliminata'), '\\Trash');
184
- test.equal(byName('Verzonden items'), '\\Sent');
185
- test.equal(byName('Elementy usunięte'), '\\Trash');
186
- test.equal(byName('Elemente șterse'), '\\Trash');
187
- test.equal(byName('Sendte elementer'), '\\Sent');
188
- test.equal(byName('Gönderilmiş Öğeler'), '\\Sent');
189
- test.equal(byName('Önemsiz E-posta'), '\\Junk');
190
- test.equal(byName('送信済みアイテム'), '\\Sent');
191
- test.equal(byName('削除済みアイテム'), '\\Trash');
192
- test.equal(byName('보낸 편지함'), '\\Sent');
193
- test.done();
194
- };
195
-
196
- // Names harvested from the localization catalogs of Roundcube, SOGo and Thunderbird.
197
- // These are the cases that motivated, and are now covered without, approximate matching.
198
- module.exports['Special Use: names taken from mail client localization catalogs'] = test => {
199
- test.equal(byName('Gesendet'), '\\Sent'); // de, Thunderbird
200
- test.equal(byName('Skickat'), '\\Sent'); // sv
201
- test.equal(byName('Gelöscht'), '\\Trash'); // de, Roundcube
202
- test.equal(byName('Poubelle'), '\\Trash'); // fr
203
- test.equal(byName('Paperera'), '\\Trash'); // ca
204
- test.equal(byName('Rämps'), '\\Junk'); // et
205
- test.equal(byName('Skräp'), '\\Junk'); // sv
206
- test.equal(byName('Ongewenst'), '\\Junk'); // nl
207
- test.equal(byName('Basura'), '\\Junk'); // es
208
- test.equal(byName('Arkisto'), '\\Archive'); // fi
209
- test.done();
210
- };
211
-
212
- // ============================================
213
- // Relaxed matching (decorated names and morphological variants)
214
- // ============================================
215
-
216
- const sourceOf = name => specialUse.specialUse(false, { flags: new Set(), name }).source;
217
-
218
- // Approximate matching is deliberately NOT done. A shared prefix is not enough
219
- // evidence: at five characters English "conceal" reaches Dutch "concepten" and
220
- // "article" reaches Romanian "articole", and a wrongly claimed Trash or Junk folder
221
- // makes a client delete into, or permanently expunge from, an ordinary folder.
222
- // Morphological variants belong in the tables as real entries instead.
223
- module.exports['Special Use: morphological variants are not guessed'] = test => {
224
- test.equal(byName('Prügikast'), '\\Trash'); // exact table entry
225
- test.equal(byName('Prügi'), null); // truncation, not matched
226
- test.equal(byName('Prügikorv'), null); // different ending, not matched
227
- test.equal(byName('Conceal'), null);
228
- test.equal(byName('Article'), null);
229
- test.equal(byName('Postal'), null);
230
- test.equal(byName('Element'), null);
231
- test.done();
232
- };
233
-
234
- // Known name decorated with a generic mail noun that carries no meaning of its own.
235
- module.exports['Special Use: names decorated with generic mail nouns'] = test => {
236
- test.equal(byName('Sent Mail'), '\\Sent');
237
- test.equal(byName('Отправленные письма'), '\\Sent');
238
- test.equal(byName('Удаленные элементы'), '\\Trash');
239
- test.equal(byName('Deleted Mail'), '\\Trash');
240
- test.equal(byName('Spam Messages'), '\\Junk');
241
- test.equal(byName('Черновики письма'), '\\Drafts');
242
- test.equal(byName('My Drafts'), '\\Drafts');
243
- test.equal(byName('Saadetud e-kirjad'), '\\Sent'); // hyphenated "e-" family
244
- test.done();
245
- };
246
-
247
- // Relaxed hits are a guess and must be reported as a distinct, lower priority source
248
- // so that an exactly named folder wins the slot when both exist in one mailbox.
249
- module.exports['Special Use: relaxed matches report a distinct source'] = test => {
250
- test.equal(sourceOf('Prügikast'), 'name');
251
- test.equal(sourceOf('Sent'), 'name');
252
- test.equal(sourceOf('Отправленные письма'), 'name-guess');
253
- test.equal(sourceOf('Sent Mail'), 'name-guess');
254
- test.done();
255
- };
256
-
257
- // The precision guards. A wrongly flagged Trash or Junk folder is destructive, so
258
- // anything that leaves more than one meaningful word behind must be refused.
259
- module.exports['Special Use: user folders that merely contain a known word are refused'] = test => {
260
- test.equal(byName('Sent to clients'), null);
261
- test.equal(byName('Archive 2023'), null);
262
- test.equal(byName('Junk food recipes'), null);
263
- test.equal(byName('Drafts of my novel'), null);
264
- test.equal(byName('Trash talk'), null);
265
- test.equal(byName('Spam reports'), null);
266
- test.equal(byName('Deleted scenes'), null);
267
- test.equal(byName('Corbeilles de fruits'), null);
268
- test.done();
269
- };
270
-
271
- // Ordinary words that share a prefix with an entry must never be classified.
272
- module.exports['Special Use: words sharing only a prefix do not match'] = test => {
273
- test.equal(byName('Draftsman'), null);
274
- test.equal(byName('Sentinel'), null);
275
- test.equal(byName('Sentiments'), null);
276
- test.equal(byName('Junkyard'), null);
277
- test.equal(byName('Spammers'), null);
278
- test.equal(byName('Binder'), null);
279
- test.equal(byName('Postbox'), null);
280
- test.equal(byName('Papers'), null);
281
- test.done();
282
- };
283
-
284
- // Every source specialUse() can return has to be ranked by the conflict resolution in
285
- // lib/commands/list.js. A new tier added here without being added there would silently
286
- // sort ahead of an explicit user hint, so pin the vocabulary from this side.
287
- module.exports['Special Use: reports only sources that list.js ranks'] = test => {
288
- const RANKED = ['user', 'extension', 'name', 'name-guess'];
289
- const listSource = fs.readFileSync(`${__dirname}/../lib/commands/list.js`, 'utf8');
290
- const declared = listSource.match(/const SOURCE_SORT_ORDER = \[([^\]]*)\]/);
291
-
292
- test.ok(declared, 'SOURCE_SORT_ORDER not found in lib/commands/list.js');
293
- test.deepEqual(
294
- declared[1]
295
- .split(',')
296
- .map(part => part.trim().replace(/^'|'$/g, ''))
297
- .filter(Boolean),
298
- RANKED
299
- );
300
-
301
- // and every source this module actually emits is one of them
302
- const emitted = new Set();
303
- emitted.add(specialUse.specialUse(true, { flags: new Set(['\\Sent']), name: 'x' }).source);
304
- emitted.add(specialUse.specialUse(false, { flags: new Set(), name: 'Sent' }).source);
305
- emitted.add(specialUse.specialUse(false, { flags: new Set(), name: 'Sent Mail' }).source);
306
- for (let source of emitted) {
307
- test.ok(RANKED.includes(source), `unranked source: ${source}`);
308
- }
309
- test.done();
310
- };
311
-
312
- // ============================================
313
- // Structural guards for the name tables
314
- // ============================================
315
-
316
- // A name listed under two different flags would make detection depend on key order.
317
- module.exports['Special Use: no folder name is claimed by two flags'] = test => {
318
- let owner = new Map();
319
- let collisions = [];
320
- for (let flag of Object.keys(specialUse.names)) {
321
- for (let name of specialUse.names[flag]) {
322
- if (owner.has(name) && owner.get(name) !== flag) {
323
- collisions.push(`${name}: ${owner.get(name)} vs ${flag}`);
324
- }
325
- owner.set(name, flag);
326
- }
327
- }
328
- test.deepEqual(collisions, []);
329
- test.done();
330
- };
331
-
332
- // Lookups normalize the incoming name to NFKC, so an entry stored in any other
333
- // normalization form can never match, however the server spells the folder.
334
- module.exports['Special Use: every name entry is stored in NFKC form'] = test => {
335
- let problems = [];
336
- for (let flag of Object.keys(specialUse.names)) {
337
- for (let name of specialUse.names[flag]) {
338
- if (name !== name.normalize('NFKC')) {
339
- problems.push(`${flag}: not NFKC: ${name}`);
340
- }
341
- }
342
- }
343
- test.deepEqual(problems, []);
344
- test.done();
345
- };
346
-
347
- // Servers echo back whatever normalization form the creating client used, so both
348
- // canonically equivalent spellings of a name must resolve to the same flag.
349
- module.exports['Special Use: matching is independent of Unicode normalization form'] = test => {
350
- let mismatches = [];
351
- for (let flag of Object.keys(specialUse.names)) {
352
- for (let name of specialUse.names[flag]) {
353
- for (let form of ['NFC', 'NFD', 'NFKC', 'NFKD']) {
354
- let resolved = specialUse.specialUse(false, { flags: new Set(), name: name.normalize(form) }).flag;
355
- if (resolved !== flag) {
356
- mismatches.push(`${flag}: "${name}" as ${form} resolved to ${resolved}`);
357
- }
358
- }
359
- }
360
- }
361
- test.deepEqual(mismatches, []);
362
- test.done();
363
- };
364
-
365
- // Compatibility folding is the reason lookups normalize with NFKC rather than NFC.
366
- // The Japanese Sent entry was stored with halfwidth katakana, which NFC does not
367
- // fold, so it never matched the fullwidth spelling that servers actually send.
368
- module.exports['Special Use: halfwidth and fullwidth forms fold onto the same entry'] = test => {
369
- // U+FF92 U+FF70 U+FF99 halfwidth vs U+30E1 U+30FC U+30EB fullwidth katakana
370
- const halfwidth = '送信済み' + String.fromCodePoint(0xff92, 0xff70, 0xff99);
371
- const fullwidth = '送信済み' + String.fromCodePoint(0x30e1, 0x30fc, 0x30eb);
372
-
373
- test.equal(byName(halfwidth), '\\Sent');
374
- test.equal(byName(fullwidth), '\\Sent');
375
- // Fullwidth Latin, which some CJK clients use for ASCII folder names
376
- test.equal(byName('Sent'), '\\Sent');
377
- test.equal(byName('Trash'), '\\Trash');
378
- test.done();
379
- };
380
-
381
- // Devanagari and Bengali nukta letters are Unicode composition exclusions: the
382
- // precomposed character is NOT the NFC form, so these two entries used to match
383
- // only if the server happened to send the precomposed spelling.
384
- module.exports['Special Use: nukta drafts folders match in either spelling'] = test => {
385
- // U+095E DEVANAGARI LETTER PHA WITH NUKTA vs U+092B U+093C
386
- const hindi = String.fromCodePoint(0x0921, 0x094d, 0x0930, 0x093e, 0x095e, 0x094d, 0x091f);
387
- // U+09DC BENGALI LETTER RRA vs U+09A1 U+09BC
388
- const bengali = String.fromCodePoint(0x0996, 0x09b8, 0x09dc, 0x09be);
389
-
390
- test.equal(byName(hindi), '\\Drafts');
391
- test.equal(byName(hindi.normalize('NFC')), '\\Drafts');
392
- test.equal(byName(bengali), '\\Drafts');
393
- test.equal(byName(bengali.normalize('NFC')), '\\Drafts');
394
- test.done();
395
- };
396
-
397
- // Lookups compare against a lowercased and trimmed name, so entries stored in any
398
- // other form are dead weight that can never match.
399
- module.exports['Special Use: every name entry is lowercase, trimmed and unique'] = test => {
400
- let problems = [];
401
- for (let flag of Object.keys(specialUse.names)) {
402
- let seen = new Set();
403
- for (let name of specialUse.names[flag]) {
404
- if (name !== name.toLowerCase()) {
405
- problems.push(`${flag}: not lowercase: ${name}`);
406
- }
407
- if (name !== name.trim()) {
408
- problems.push(`${flag}: not trimmed: ${JSON.stringify(name)}`);
409
- }
410
- if (seen.has(name)) {
411
- problems.push(`${flag}: duplicate: ${name}`);
412
- }
413
- seen.add(name);
414
- }
415
- }
416
- test.deepEqual(problems, []);
417
- test.done();
418
- };
@@ -1,181 +0,0 @@
1
- 'use strict';
2
-
3
- const net = require('net');
4
- const { ImapFlow } = require('../lib/imap-flow');
5
- const { ImapStream } = require('../lib/handler/imap-stream');
6
-
7
- // Minimal pre-STARTTLS IMAP server. Sends the greeting, advertises STARTTLS, and on the
8
- // STARTTLS command invokes `onStartTls(socket, tag)` so each test controls what (if
9
- // anything) is written after the tagged OK. It never performs a real TLS handshake.
10
- const createStartTlsServer = onStartTls =>
11
- net.createServer(socket => {
12
- socket.on('error', () => {});
13
- socket.write('* OK [CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED] ready\r\n');
14
-
15
- let buf = '';
16
- socket.on('data', data => {
17
- buf += data.toString('binary');
18
- let idx;
19
- while ((idx = buf.indexOf('\r\n')) >= 0) {
20
- let line = buf.slice(0, idx);
21
- buf = buf.slice(idx + 2);
22
- let parts = line.split(' ');
23
- let tag = parts[0];
24
- let command = (parts[1] || '').toUpperCase();
25
- if (command === 'CAPABILITY') {
26
- socket.write('* CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED\r\n');
27
- socket.write(`${tag} OK CAPABILITY done\r\n`);
28
- } else if (command === 'STARTTLS') {
29
- onStartTls(socket, tag);
30
- }
31
- }
32
- });
33
- });
34
-
35
- const makeClient = port =>
36
- new ImapFlow({
37
- host: '127.0.0.1',
38
- port,
39
- secure: false,
40
- doSTARTTLS: true,
41
- servername: 'localhost',
42
- tls: { rejectUnauthorized: false },
43
- disableAutoIdle: true,
44
- logger: false,
45
- auth: { user: 'test', pass: 'test' }
46
- });
47
-
48
- // A MITM injects a response in the SAME segment as the STARTTLS OK, before the handshake.
49
- // This is caught by the parser-level trailing-data flag (Check 1 in upgradeToSTARTTLS).
50
- // Injection that instead arrives after the OK in a separate segment is caught by the
51
- // socket-level read after unpipe (Check 2) or, failing that, by TLS handshake corruption;
52
- // those fragmented cases are timing-dependent and not asserted deterministically here.
53
- module.exports['STARTTLS: rejects same-segment plaintext injection'] = async test => {
54
- let server = createStartTlsServer((socket, tag) => {
55
- // OK and an injected untagged CAPABILITY in a single write, no TLS handshake.
56
- socket.write(`${tag} OK Begin TLS\r\n* CAPABILITY IMAP4rev1 INJECTED\r\n`);
57
- });
58
-
59
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
60
- let port = server.address().port;
61
-
62
- let client = makeClient(port);
63
- client.on('error', () => {});
64
-
65
- let connectErr = null;
66
- try {
67
- await client.connect();
68
- test.ok(false, 'connect() must reject when data is injected after the STARTTLS OK');
69
- } catch (err) {
70
- connectErr = err;
71
- }
72
-
73
- test.ok(connectErr, 'connect() rejected');
74
- test.equal(connectErr.code, 'STARTTLS_INJECTION', 'rejection is flagged as an injection');
75
- test.ok(connectErr.tlsFailed, 'rejection is treated as a TLS failure');
76
- // The connection must fail closed: it never becomes usable and never authenticates.
77
- // (The injected untagged response may be parsed transiently before teardown, so asserting
78
- // it never reaches the capability set would be a timing-fragile false assurance.)
79
- test.ok(!client.usable, 'the connection did not become usable (failed closed)');
80
- test.ok(!client.authenticated, 'the connection never authenticated');
81
-
82
- client.close();
83
- server.close();
84
- test.done();
85
- };
86
-
87
- // A compliant server stays silent after the STARTTLS OK. The injection guard must NOT
88
- // fire (no false positive); the upgrade proceeds to the TLS handshake, which then fails
89
- // here only because this stub server never speaks TLS — proving the guard let it through.
90
- module.exports['STARTTLS: clean OK is not flagged as injection'] = async test => {
91
- let server = createStartTlsServer((socket, tag) => {
92
- // Only the tagged OK, nothing after it, then drop the connection.
93
- socket.write(`${tag} OK Begin TLS\r\n`);
94
- setImmediate(() => socket.destroy());
95
- });
96
-
97
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
98
- let port = server.address().port;
99
-
100
- let client = makeClient(port);
101
- client.on('error', () => {});
102
-
103
- let connectErr = null;
104
- try {
105
- await client.connect();
106
- test.ok(false, 'connect() rejects because the stub never completes the TLS handshake');
107
- } catch (err) {
108
- connectErr = err;
109
- }
110
-
111
- test.ok(connectErr, 'connect() rejected (no TLS handshake on the stub server)');
112
- test.notEqual(connectErr.code, 'STARTTLS_INJECTION', 'a clean OK must not be flagged as injection');
113
-
114
- client.close();
115
- server.close();
116
- test.done();
117
- };
118
-
119
- // A STARTTLS handshake failure must reject connect() through a single error path: it must
120
- // not also emit an 'error' event (which would double-report and crash a listener-less client).
121
- module.exports['STARTTLS: handshake failure has a single error path'] = async test => {
122
- let server = createStartTlsServer((socket, tag) => {
123
- // Acknowledge STARTTLS, then drop the connection instead of doing TLS.
124
- socket.write(`${tag} OK Begin TLS\r\n`);
125
- setImmediate(() => socket.destroy());
126
- });
127
-
128
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
129
- let port = server.address().port;
130
-
131
- let client = makeClient(port);
132
-
133
- // A second error path would emit here (and crash a listener-less client).
134
- let errorEvents = 0;
135
- client.on('error', () => {
136
- errorEvents++;
137
- });
138
-
139
- let connectErr = null;
140
- try {
141
- await client.connect();
142
- test.ok(false, 'connect() should reject on STARTTLS handshake failure');
143
- } catch (err) {
144
- connectErr = err;
145
- }
146
-
147
- // Give any stray async error handler a chance to (wrongly) fire.
148
- await new Promise(r => setTimeout(r, 50));
149
-
150
- test.ok(connectErr, 'connect() rejected');
151
- test.ok(connectErr.tlsFailed, 'rejection is flagged as a TLS failure');
152
- test.equal(errorEvents, 0, 'no duplicate error event emitted (single error path)');
153
-
154
- client.close();
155
- server.close();
156
- test.done();
157
- };
158
-
159
- // Unit-level check of the per-command trailing flag the guard relies on: each pushed
160
- // command records whether more input followed it, independently of later commands.
161
- module.exports['STARTTLS: parser flags trailing data per command'] = test => {
162
- const stream = new ImapStream({ cid: 'test' });
163
- let flags = [];
164
-
165
- stream.on('readable', () => {
166
- let cmd;
167
- while ((cmd = stream.read()) !== null) {
168
- flags.push(cmd.trailingAfterLine);
169
- cmd.next();
170
- }
171
- });
172
- stream.on('error', err => test.ifError(err));
173
- stream.on('end', () => {
174
- // First command had a second command after it -> true; the last one -> false.
175
- test.deepEqual(flags, [true, false], 'trailingAfterLine is per-command and not overwritten');
176
- test.done();
177
- });
178
-
179
- // Two complete commands in a single write.
180
- stream.end(Buffer.from('A OK first\r\nB OK second\r\n'));
181
- };