imapflow 1.7.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +8 -2
  3. package/dist/cjs/charsets.d.ts +1 -0
  4. package/dist/cjs/charsets.js +294 -0
  5. package/dist/cjs/commands/append.d.ts +22 -0
  6. package/dist/cjs/commands/append.js +151 -0
  7. package/dist/cjs/commands/authenticate.d.ts +24 -0
  8. package/dist/cjs/commands/authenticate.js +223 -0
  9. package/dist/cjs/commands/capability.d.ts +8 -0
  10. package/dist/cjs/commands/capability.js +32 -0
  11. package/dist/cjs/commands/close.d.ts +8 -0
  12. package/dist/cjs/commands/close.js +39 -0
  13. package/dist/cjs/commands/compress.d.ts +8 -0
  14. package/dist/cjs/commands/compress.js +56 -0
  15. package/dist/cjs/commands/copy.d.ts +13 -0
  16. package/dist/cjs/commands/copy.js +44 -0
  17. package/dist/cjs/commands/copyuid-parser.d.ts +11 -0
  18. package/dist/cjs/commands/copyuid-parser.js +32 -0
  19. package/dist/cjs/commands/create.d.ts +11 -0
  20. package/dist/cjs/commands/create.js +80 -0
  21. package/dist/cjs/commands/delete.d.ts +11 -0
  22. package/dist/cjs/commands/delete.js +40 -0
  23. package/dist/cjs/commands/enable.d.ts +9 -0
  24. package/dist/cjs/commands/enable.js +61 -0
  25. package/dist/cjs/commands/esearch-parser.d.ts +17 -0
  26. package/dist/cjs/commands/esearch-parser.js +91 -0
  27. package/dist/cjs/commands/expunge.d.ts +12 -0
  28. package/dist/cjs/commands/expunge.js +60 -0
  29. package/dist/cjs/commands/fetch.d.ts +30 -0
  30. package/dist/cjs/commands/fetch.js +241 -0
  31. package/dist/cjs/commands/id.d.ts +10 -0
  32. package/dist/cjs/commands/id.js +80 -0
  33. package/dist/cjs/commands/idle.d.ts +9 -0
  34. package/dist/cjs/commands/idle.js +347 -0
  35. package/dist/cjs/commands/list.d.ts +16 -0
  36. package/dist/cjs/commands/list.js +518 -0
  37. package/dist/cjs/commands/login.d.ts +11 -0
  38. package/dist/cjs/commands/login.js +42 -0
  39. package/dist/cjs/commands/logout.d.ts +8 -0
  40. package/dist/cjs/commands/logout.js +47 -0
  41. package/dist/cjs/commands/move.d.ts +13 -0
  42. package/dist/cjs/commands/move.js +57 -0
  43. package/dist/cjs/commands/namespace.d.ts +25 -0
  44. package/dist/cjs/commands/namespace.js +139 -0
  45. package/dist/cjs/commands/noop.d.ts +8 -0
  46. package/dist/cjs/commands/noop.js +22 -0
  47. package/dist/cjs/commands/quota.d.ts +10 -0
  48. package/dist/cjs/commands/quota.js +119 -0
  49. package/dist/cjs/commands/rename.d.ts +12 -0
  50. package/dist/cjs/commands/rename.js +48 -0
  51. package/dist/cjs/commands/search.d.ts +15 -0
  52. package/dist/cjs/commands/search.js +228 -0
  53. package/dist/cjs/commands/select.d.ts +25 -0
  54. package/dist/cjs/commands/select.js +250 -0
  55. package/dist/cjs/commands/starttls.d.ts +8 -0
  56. package/dist/cjs/commands/starttls.js +30 -0
  57. package/dist/cjs/commands/status-fields.d.ts +14 -0
  58. package/dist/cjs/commands/status-fields.js +61 -0
  59. package/dist/cjs/commands/status.d.ts +12 -0
  60. package/dist/cjs/commands/status.js +108 -0
  61. package/dist/cjs/commands/store.d.ts +19 -0
  62. package/dist/cjs/commands/store.js +93 -0
  63. package/dist/cjs/commands/subscribe.d.ts +9 -0
  64. package/dist/cjs/commands/subscribe.js +31 -0
  65. package/dist/cjs/commands/unsubscribe.d.ts +9 -0
  66. package/dist/cjs/commands/unsubscribe.js +31 -0
  67. package/dist/cjs/connection-deadline.d.ts +49 -0
  68. package/dist/cjs/connection-deadline.js +91 -0
  69. package/dist/cjs/errors.d.ts +83 -0
  70. package/dist/cjs/errors.js +13 -0
  71. package/dist/cjs/handler/imap-compiler.d.ts +24 -0
  72. package/dist/cjs/handler/imap-compiler.js +285 -0
  73. package/dist/cjs/handler/imap-formal-syntax.d.ts +28 -0
  74. package/dist/cjs/handler/imap-formal-syntax.js +121 -0
  75. package/dist/cjs/handler/imap-handler.d.ts +9 -0
  76. package/dist/cjs/handler/imap-handler.js +10 -0
  77. package/dist/cjs/handler/imap-parser.d.ts +16 -0
  78. package/dist/cjs/handler/imap-parser.js +90 -0
  79. package/dist/cjs/handler/imap-stream.d.ts +181 -0
  80. package/dist/cjs/handler/imap-stream.js +446 -0
  81. package/dist/cjs/handler/limits.d.ts +25 -0
  82. package/dist/cjs/handler/limits.js +51 -0
  83. package/dist/cjs/handler/parser-instance.d.ts +68 -0
  84. package/dist/cjs/handler/parser-instance.js +223 -0
  85. package/dist/cjs/handler/token-parser.d.ts +91 -0
  86. package/dist/cjs/handler/token-parser.js +673 -0
  87. package/dist/cjs/handler/types.d.ts +91 -0
  88. package/dist/cjs/handler/types.js +4 -0
  89. package/dist/cjs/imap-commands.d.ts +16 -0
  90. package/dist/cjs/imap-commands.js +74 -0
  91. package/dist/cjs/imap-flow.d.ts +676 -0
  92. package/dist/cjs/imap-flow.js +3949 -0
  93. package/dist/cjs/jp-decoder.d.ts +12 -0
  94. package/dist/cjs/jp-decoder.js +79 -0
  95. package/dist/cjs/limited-passthrough.d.ts +25 -0
  96. package/dist/cjs/limited-passthrough.js +54 -0
  97. package/dist/cjs/logger.d.ts +3 -0
  98. package/dist/cjs/logger.js +11 -0
  99. package/dist/cjs/package-info.d.ts +3 -0
  100. package/dist/cjs/package-info.js +7 -0
  101. package/dist/cjs/package.json +3 -0
  102. package/dist/cjs/proxy-connection.d.ts +33 -0
  103. package/dist/cjs/proxy-connection.js +392 -0
  104. package/dist/cjs/search-compiler.d.ts +34 -0
  105. package/dist/cjs/search-compiler.js +476 -0
  106. package/dist/cjs/special-use.d.ts +22 -0
  107. package/dist/cjs/special-use.js +911 -0
  108. package/dist/cjs/tools.d.ts +427 -0
  109. package/dist/cjs/tools.js +1496 -0
  110. package/{lib/imap-flow.d.ts → dist/cjs/types.d.ts} +386 -516
  111. package/dist/cjs/types.js +5 -0
  112. package/dist/esm/charsets.d.ts +1 -0
  113. package/{lib → dist/esm}/charsets.js +1 -6
  114. package/dist/esm/commands/append.d.ts +22 -0
  115. package/{lib → dist/esm}/commands/append.js +22 -52
  116. package/dist/esm/commands/authenticate.d.ts +24 -0
  117. package/{lib → dist/esm}/commands/authenticate.js +62 -87
  118. package/dist/esm/commands/capability.d.ts +8 -0
  119. package/{lib → dist/esm}/commands/capability.js +6 -9
  120. package/dist/esm/commands/close.d.ts +8 -0
  121. package/{lib → dist/esm}/commands/close.js +6 -10
  122. package/dist/esm/commands/compress.d.ts +8 -0
  123. package/{lib → dist/esm}/commands/compress.js +7 -11
  124. package/dist/esm/commands/copy.d.ts +13 -0
  125. package/{lib → dist/esm}/commands/copy.js +12 -20
  126. package/dist/esm/commands/copyuid-parser.d.ts +11 -0
  127. package/{lib → dist/esm}/commands/copyuid-parser.js +9 -15
  128. package/dist/esm/commands/create.d.ts +11 -0
  129. package/{lib → dist/esm}/commands/create.js +13 -27
  130. package/dist/esm/commands/delete.d.ts +11 -0
  131. package/{lib → dist/esm}/commands/delete.js +9 -14
  132. package/dist/esm/commands/enable.d.ts +9 -0
  133. package/{lib → dist/esm}/commands/enable.js +23 -30
  134. package/dist/esm/commands/esearch-parser.d.ts +17 -0
  135. package/dist/esm/commands/esearch-parser.js +88 -0
  136. package/dist/esm/commands/expunge.d.ts +12 -0
  137. package/{lib → dist/esm}/commands/expunge.js +17 -22
  138. package/dist/esm/commands/fetch.d.ts +30 -0
  139. package/{lib → dist/esm}/commands/fetch.js +32 -64
  140. package/dist/esm/commands/id.d.ts +10 -0
  141. package/{lib → dist/esm}/commands/id.js +17 -23
  142. package/dist/esm/commands/idle.d.ts +9 -0
  143. package/{lib → dist/esm}/commands/idle.js +47 -81
  144. package/dist/esm/commands/list.d.ts +16 -0
  145. package/{lib → dist/esm}/commands/list.js +56 -121
  146. package/dist/esm/commands/login.d.ts +11 -0
  147. package/{lib → dist/esm}/commands/login.js +10 -15
  148. package/dist/esm/commands/logout.d.ts +8 -0
  149. package/{lib → dist/esm}/commands/logout.js +9 -11
  150. package/dist/esm/commands/move.d.ts +13 -0
  151. package/{lib → dist/esm}/commands/move.js +13 -21
  152. package/dist/esm/commands/namespace.d.ts +25 -0
  153. package/{lib → dist/esm}/commands/namespace.js +34 -44
  154. package/dist/esm/commands/noop.d.ts +8 -0
  155. package/{lib → dist/esm}/commands/noop.js +6 -7
  156. package/dist/esm/commands/quota.d.ts +10 -0
  157. package/{lib → dist/esm}/commands/quota.js +18 -36
  158. package/dist/esm/commands/rename.d.ts +12 -0
  159. package/{lib → dist/esm}/commands/rename.js +10 -15
  160. package/dist/esm/commands/search.d.ts +15 -0
  161. package/{lib → dist/esm}/commands/search.js +36 -135
  162. package/dist/esm/commands/select.d.ts +25 -0
  163. package/{lib → dist/esm}/commands/select.js +33 -64
  164. package/dist/esm/commands/starttls.d.ts +8 -0
  165. package/{lib → dist/esm}/commands/starttls.js +6 -8
  166. package/dist/esm/commands/status-fields.d.ts +14 -0
  167. package/{lib → dist/esm}/commands/status-fields.js +5 -16
  168. package/dist/esm/commands/status.d.ts +12 -0
  169. package/{lib → dist/esm}/commands/status.js +18 -29
  170. package/dist/esm/commands/store.d.ts +19 -0
  171. package/{lib → dist/esm}/commands/store.js +24 -37
  172. package/dist/esm/commands/subscribe.d.ts +9 -0
  173. package/{lib → dist/esm}/commands/subscribe.js +8 -12
  174. package/dist/esm/commands/unsubscribe.d.ts +9 -0
  175. package/{lib → dist/esm}/commands/unsubscribe.js +8 -12
  176. package/dist/esm/connection-deadline.d.ts +49 -0
  177. package/{lib → dist/esm}/connection-deadline.js +14 -25
  178. package/dist/esm/errors.d.ts +83 -0
  179. package/dist/esm/errors.js +9 -0
  180. package/dist/esm/handler/imap-compiler.d.ts +24 -0
  181. package/{lib → dist/esm}/handler/imap-compiler.js +22 -80
  182. package/dist/esm/handler/imap-formal-syntax.d.ts +28 -0
  183. package/dist/esm/handler/imap-formal-syntax.js +117 -0
  184. package/dist/esm/handler/imap-handler.d.ts +9 -0
  185. package/dist/esm/handler/imap-handler.js +9 -0
  186. package/dist/esm/handler/imap-parser.d.ts +16 -0
  187. package/{lib → dist/esm}/handler/imap-parser.js +31 -44
  188. package/dist/esm/handler/imap-stream.d.ts +181 -0
  189. package/{lib → dist/esm}/handler/imap-stream.js +29 -121
  190. package/dist/esm/handler/limits.d.ts +25 -0
  191. package/{lib → dist/esm}/handler/limits.js +13 -22
  192. package/dist/esm/handler/parser-instance.d.ts +68 -0
  193. package/{lib → dist/esm}/handler/parser-instance.js +19 -47
  194. package/dist/esm/handler/token-parser.d.ts +91 -0
  195. package/{lib → dist/esm}/handler/token-parser.js +71 -155
  196. package/dist/esm/handler/types.d.ts +91 -0
  197. package/dist/esm/handler/types.js +3 -0
  198. package/dist/esm/imap-commands.d.ts +16 -0
  199. package/dist/esm/imap-commands.js +67 -0
  200. package/dist/esm/imap-flow.d.ts +676 -0
  201. package/{lib → dist/esm}/imap-flow.js +785 -1802
  202. package/dist/esm/jp-decoder.d.ts +12 -0
  203. package/{lib → dist/esm}/jp-decoder.js +6 -21
  204. package/dist/esm/limited-passthrough.d.ts +25 -0
  205. package/{lib → dist/esm}/limited-passthrough.js +7 -20
  206. package/dist/esm/logger.d.ts +3 -0
  207. package/dist/esm/logger.js +4 -0
  208. package/dist/esm/package-info.d.ts +3 -0
  209. package/dist/esm/package-info.js +4 -0
  210. package/dist/esm/package.json +3 -0
  211. package/dist/esm/proxy-connection.d.ts +33 -0
  212. package/{lib → dist/esm}/proxy-connection.js +56 -127
  213. package/dist/esm/search-compiler.d.ts +34 -0
  214. package/{lib → dist/esm}/search-compiler.js +54 -110
  215. package/dist/esm/special-use.d.ts +22 -0
  216. package/dist/esm/special-use.js +907 -0
  217. package/dist/esm/tools.d.ts +427 -0
  218. package/dist/esm/tools.js +1446 -0
  219. package/dist/esm/types.d.ts +828 -0
  220. package/dist/esm/types.js +4 -0
  221. package/package.json +60 -20
  222. package/.gitattributes +0 -1
  223. package/.github/CODE_OF_CONDUCT.md +0 -76
  224. package/.github/FUNDING.yml +0 -4
  225. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  226. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  227. package/.github/contributing.md +0 -17
  228. package/.github/workflows/release.yaml +0 -36
  229. package/.github/workflows/stale.yml +0 -29
  230. package/.github/workflows/test.yml +0 -51
  231. package/.ncurc.js +0 -4
  232. package/.prettierignore +0 -4
  233. package/.prettierrc.js +0 -8
  234. package/.release-please-manifest.json +0 -3
  235. package/CLAUDE.md +0 -104
  236. package/Gruntfile.js +0 -23
  237. package/eslint.config.js +0 -45
  238. package/lib/handler/imap-formal-syntax.js +0 -189
  239. package/lib/handler/imap-handler.js +0 -17
  240. package/lib/imap-commands.js +0 -45
  241. package/lib/logger.js +0 -5
  242. package/lib/special-use.js +0 -923
  243. package/lib/tools.js +0 -1612
  244. package/release-please-config.json +0 -10
  245. package/test/authentication-test.js +0 -101
  246. package/test/auto-idle-test.js +0 -470
  247. package/test/bodystructure-test.js +0 -899
  248. package/test/charsets-test.js +0 -161
  249. package/test/commands-branches-test.js +0 -1095
  250. package/test/commands-integration-test.js +0 -11124
  251. package/test/commands-test.js +0 -73
  252. package/test/connection-edge-cases-test.js +0 -1828
  253. package/test/connection-test.js +0 -162
  254. package/test/copyuid-parser-test.js +0 -173
  255. package/test/fetch-generator-test.js +0 -218
  256. package/test/fixtures/fake-timers.js +0 -115
  257. package/test/fixtures/serialized-mimetorture.js +0 -2738
  258. package/test/fixtures/test-client.js +0 -57
  259. package/test/fixtures/test-tls.js +0 -8
  260. package/test/handler-branches-test.js +0 -310
  261. package/test/idle-polling-test.js +0 -518
  262. package/test/imap-compiler-test.js +0 -809
  263. package/test/imap-flow-compress-test.js +0 -166
  264. package/test/imap-flow-coverage-test.js +0 -612
  265. package/test/imap-flow-fetch-download-test.js +0 -873
  266. package/test/imap-flow-internals-test.js +0 -725
  267. package/test/imap-flow-methods-test.js +0 -889
  268. package/test/imap-flow-proxy-paths-test.js +0 -366
  269. package/test/imap-flow-secure-test.js +0 -573
  270. package/test/imap-flow-server-test.js +0 -1474
  271. package/test/imap-formal-syntax-test.js +0 -293
  272. package/test/imap-parser-test.js +0 -1474
  273. package/test/imap-stream-edge-cases-test.js +0 -666
  274. package/test/imap-stream-test.js +0 -177
  275. package/test/imapflow-test.js +0 -258
  276. package/test/integration/README.md +0 -52
  277. package/test/integration/dovecot-test.conf +0 -27
  278. package/test/integration/rev2-live-test.js +0 -431
  279. package/test/integration/run-rev2-tests.sh +0 -75
  280. package/test/integration-test.js +0 -83
  281. package/test/jp-decoder-test.js +0 -304
  282. package/test/limited-passthrough-test.js +0 -299
  283. package/test/memory-cleanup-test.js +0 -144
  284. package/test/memory-leak-test.js +0 -667
  285. package/test/parser-limits-test.js +0 -292
  286. package/test/proxy-connection-test.js +0 -738
  287. package/test/reliability-improvements-test.js +0 -548
  288. package/test/search-compiler-test.js +0 -1300
  289. package/test/search-test.js +0 -329
  290. package/test/special-use-test.js +0 -418
  291. package/test/starttls-injection-test.js +0 -181
  292. package/test/tag-correlation-test.js +0 -333
  293. package/test/timer-policy-test.js +0 -227
  294. package/test/token-parser-test.js +0 -456
  295. package/test/tools-test.js +0 -2013
  296. package/test/unhandled-rejection-test.js +0 -593
@@ -1,1300 +0,0 @@
1
- 'use strict';
2
-
3
- const { searchCompiler } = require('../lib/search-compiler');
4
-
5
- // Mock mailbox for testing
6
- let createMockMailbox = () => ({
7
- flags: new Set(['\\Seen', '\\Answered', '\\Flagged', '\\Deleted', '\\Draft', '$CustomFlag']),
8
- permanentFlags: new Set(['\\*'])
9
- });
10
-
11
- // Helper to create mock connection with customizable capabilities
12
- let createMockConnection = (options = {}) => ({
13
- capabilities: new Map(options.capabilities || [['IMAP4rev1', true]]),
14
- enabled: new Set(options.enabled || []),
15
- mailbox: options.mailbox || createMockMailbox()
16
- });
17
-
18
- // Helper to find attribute by value (recurses into sub-arrays for parenthesized groups)
19
- let findAttr = (attrs, value) => {
20
- for (let a of attrs) {
21
- if (Array.isArray(a)) {
22
- let found = findAttr(a, value);
23
- if (found) return found;
24
- } else if (a.value === value) {
25
- return a;
26
- }
27
- }
28
- return undefined;
29
- };
30
- let hasAttr = (attrs, value) => !!findAttr(attrs, value);
31
-
32
- // ============================================
33
- // Basic functionality tests
34
- // ============================================
35
-
36
- module.exports['Search Compiler: Basic functionality'] = test => {
37
- let connection = createMockConnection();
38
-
39
- test.doesNotThrow(() => {
40
- let compiled = searchCompiler(connection, { seen: false });
41
- test.ok(Array.isArray(compiled));
42
- });
43
-
44
- test.done();
45
- };
46
-
47
- module.exports['Search Compiler: Empty query'] = test => {
48
- let connection = createMockConnection();
49
- let compiled = searchCompiler(connection, {});
50
-
51
- test.ok(Array.isArray(compiled));
52
- test.equal(compiled.length, 0);
53
- test.done();
54
- };
55
-
56
- module.exports['Search Compiler: Null/undefined query'] = test => {
57
- let connection = createMockConnection();
58
-
59
- let compiled1 = searchCompiler(connection, null);
60
- test.ok(Array.isArray(compiled1));
61
-
62
- let compiled2 = searchCompiler(connection, undefined);
63
- test.ok(Array.isArray(compiled2));
64
-
65
- test.done();
66
- };
67
-
68
- // ============================================
69
- // SEQ (sequence) tests
70
- // ============================================
71
-
72
- module.exports['Search Compiler: SEQ with string'] = test => {
73
- let connection = createMockConnection();
74
- let compiled = searchCompiler(connection, { seq: '1:100' });
75
-
76
- test.ok(hasAttr(compiled, '1:100'));
77
- let seqAttr = findAttr(compiled, '1:100');
78
- test.equal(seqAttr.type, 'SEQUENCE');
79
- test.done();
80
- };
81
-
82
- module.exports['Search Compiler: SEQ with number'] = test => {
83
- let connection = createMockConnection();
84
- let compiled = searchCompiler(connection, { seq: 42 });
85
-
86
- test.ok(hasAttr(compiled, '42'));
87
- test.done();
88
- };
89
-
90
- module.exports['Search Compiler: SEQ passes invalid values through to the compiler guard'] = test => {
91
- let connection = createMockConnection();
92
-
93
- // An invalid sequence string used to be dropped silently here, which turned the
94
- // filter into an unrestricted search matching every message. It is now emitted as
95
- // a SEQUENCE token so the IMAP compiler rejects it with a coded error instead.
96
- let compiled = searchCompiler(connection, { seq: '1 2 3' });
97
- let seqAttr = compiled.find(a => a.type === 'SEQUENCE');
98
- test.ok(seqAttr, 'the sequence value must not be dropped');
99
- test.equal(seqAttr.value, '1 2 3');
100
-
101
- // Junk values pass through too, so they fail loudly at the compiler instead of
102
- // silently widening the search
103
- let junk = searchCompiler(connection, { seq: {} });
104
- test.ok(
105
- junk.find(a => a.type === 'SEQUENCE'),
106
- 'a junk value must not be dropped'
107
- );
108
-
109
- // Zero is not a valid IMAP sequence number but it is a valid filter value,
110
- // so it must reach the compiler guard rather than be dropped as falsy
111
- let zero = searchCompiler(connection, { seq: 0 });
112
- test.equal(zero.find(a => a.type === 'SEQUENCE').value, '0', 'zero must not be dropped');
113
-
114
- test.done();
115
- };
116
-
117
- module.exports['Search Compiler: SEQ array compiles to a single comma-joined set'] = test => {
118
- let connection = createMockConnection();
119
- let compiled = searchCompiler(connection, { seq: [1, 3] });
120
-
121
- let seqAttr = compiled.find(a => a.type === 'SEQUENCE');
122
- test.ok(seqAttr, 'array must produce a sequence set');
123
- test.equal(seqAttr.value, '1,3');
124
- test.done();
125
- };
126
-
127
- module.exports['Search Compiler: SEQ empty string compiles to nothing'] = test => {
128
- let connection = createMockConnection();
129
- let compiled = searchCompiler(connection, { seq: '' });
130
-
131
- test.ok(!compiled.find(a => a.type === 'SEQUENCE'), 'an empty set adds no SEQUENCE attribute');
132
- test.equal(compiled.length, 0);
133
- test.done();
134
- };
135
-
136
- // ============================================
137
- // Boolean flag tests
138
- // ============================================
139
-
140
- module.exports['Search Compiler: SEEN flag true'] = test => {
141
- let connection = createMockConnection();
142
- let compiled = searchCompiler(connection, { seen: true });
143
-
144
- test.ok(hasAttr(compiled, 'SEEN'));
145
- test.done();
146
- };
147
-
148
- module.exports['Search Compiler: SEEN flag false adds UNSEEN'] = test => {
149
- let connection = createMockConnection();
150
- let compiled = searchCompiler(connection, { seen: false });
151
-
152
- test.ok(hasAttr(compiled, 'UNSEEN'));
153
- test.done();
154
- };
155
-
156
- module.exports['Search Compiler: UNSEEN flag false adds SEEN'] = test => {
157
- let connection = createMockConnection();
158
- let compiled = searchCompiler(connection, { unseen: false });
159
-
160
- test.ok(hasAttr(compiled, 'SEEN'));
161
- test.done();
162
- };
163
-
164
- module.exports['Search Compiler: All boolean flags'] = test => {
165
- let connection = createMockConnection();
166
-
167
- // Test all toggleable flags
168
- let flags = ['answered', 'deleted', 'draft', 'flagged', 'seen'];
169
- flags.forEach(flag => {
170
- let compiled = searchCompiler(connection, { [flag]: true });
171
- test.ok(hasAttr(compiled, flag.toUpperCase()), `${flag} should be present`);
172
- });
173
-
174
- test.done();
175
- };
176
-
177
- module.exports['Search Compiler: UN-prefixed flags'] = test => {
178
- let connection = createMockConnection();
179
-
180
- let compiled = searchCompiler(connection, {
181
- unanswered: true,
182
- undeleted: true,
183
- undraft: true,
184
- unflagged: true
185
- });
186
-
187
- test.ok(hasAttr(compiled, 'UNANSWERED'));
188
- test.ok(hasAttr(compiled, 'UNDELETED'));
189
- test.ok(hasAttr(compiled, 'UNDRAFT'));
190
- test.ok(hasAttr(compiled, 'UNFLAGGED'));
191
- test.done();
192
- };
193
-
194
- // ============================================
195
- // Simple boolean flags (ALL, NEW, OLD, RECENT)
196
- // ============================================
197
-
198
- module.exports['Search Compiler: ALL flag'] = test => {
199
- let connection = createMockConnection();
200
- let compiled = searchCompiler(connection, { all: true });
201
-
202
- test.ok(hasAttr(compiled, 'ALL'));
203
- test.done();
204
- };
205
-
206
- module.exports['Search Compiler: NEW flag'] = test => {
207
- let connection = createMockConnection();
208
- let compiled = searchCompiler(connection, { new: true });
209
-
210
- test.ok(hasAttr(compiled, 'NEW'));
211
- test.done();
212
- };
213
-
214
- module.exports['Search Compiler: OLD flag'] = test => {
215
- let connection = createMockConnection();
216
- let compiled = searchCompiler(connection, { old: true });
217
-
218
- test.ok(hasAttr(compiled, 'OLD'));
219
- test.done();
220
- };
221
-
222
- module.exports['Search Compiler: RECENT flag'] = test => {
223
- let connection = createMockConnection();
224
- let compiled = searchCompiler(connection, { recent: true });
225
-
226
- test.ok(hasAttr(compiled, 'RECENT'));
227
- test.done();
228
- };
229
-
230
- module.exports['Search Compiler: NEW/OLD/RECENT throw on rev2 sessions'] = test => {
231
- // IMAP4rev2 (RFC 9051) removed the \Recent flag and these search keys -
232
- // a rev2 server would reject the whole search with a tagged BAD
233
- let connection = createMockConnection({ enabled: ['IMAP4REV2'] });
234
- for (let key of ['new', 'old', 'recent']) {
235
- try {
236
- searchCompiler(connection, { [key]: true });
237
- test.ok(false, `Should have thrown for ${key}`);
238
- } catch (err) {
239
- test.equal(err.code, 'MissingServerExtension');
240
- }
241
- }
242
- // Falsy values compile to nothing and must not throw
243
- test.equal(searchCompiler(connection, { recent: false }).length, 0);
244
- // ALL is still part of the rev2 grammar
245
- test.ok(hasAttr(searchCompiler(connection, { all: true }), 'ALL'));
246
- test.done();
247
- };
248
-
249
- module.exports['Search Compiler: Simple flags ignored when falsy'] = test => {
250
- let connection = createMockConnection();
251
- let compiled = searchCompiler(connection, {
252
- all: false,
253
- new: false,
254
- old: false,
255
- recent: false
256
- });
257
-
258
- test.equal(compiled.length, 0);
259
- test.done();
260
- };
261
-
262
- // ============================================
263
- // Numeric comparison tests
264
- // ============================================
265
-
266
- module.exports['Search Compiler: LARGER'] = test => {
267
- let connection = createMockConnection();
268
- let compiled = searchCompiler(connection, { larger: 10000 });
269
-
270
- test.ok(hasAttr(compiled, 'LARGER'));
271
- test.ok(hasAttr(compiled, '10000'));
272
- test.done();
273
- };
274
-
275
- module.exports['Search Compiler: SMALLER'] = test => {
276
- let connection = createMockConnection();
277
- let compiled = searchCompiler(connection, { smaller: 5000 });
278
-
279
- test.ok(hasAttr(compiled, 'SMALLER'));
280
- test.ok(hasAttr(compiled, '5000'));
281
- test.done();
282
- };
283
-
284
- module.exports['Search Compiler: MODSEQ'] = test => {
285
- let connection = createMockConnection();
286
- let compiled = searchCompiler(connection, { modseq: 123456 });
287
-
288
- test.ok(hasAttr(compiled, 'MODSEQ'));
289
- test.ok(hasAttr(compiled, '123456'));
290
- test.done();
291
- };
292
-
293
- module.exports['Search Compiler: Numeric ignores falsy values'] = test => {
294
- let connection = createMockConnection();
295
- let compiled = searchCompiler(connection, {
296
- larger: 0,
297
- smaller: null,
298
- modseq: undefined
299
- });
300
-
301
- test.equal(compiled.length, 0);
302
- test.done();
303
- };
304
-
305
- // ============================================
306
- // Text search tests
307
- // ============================================
308
-
309
- module.exports['Search Compiler: FROM'] = test => {
310
- let connection = createMockConnection();
311
- let compiled = searchCompiler(connection, { from: 'user@example.com' });
312
-
313
- test.ok(hasAttr(compiled, 'FROM'));
314
- test.ok(hasAttr(compiled, 'user@example.com'));
315
- test.done();
316
- };
317
-
318
- module.exports['Search Compiler: TO'] = test => {
319
- let connection = createMockConnection();
320
- let compiled = searchCompiler(connection, { to: 'recipient@example.com' });
321
-
322
- test.ok(hasAttr(compiled, 'TO'));
323
- test.ok(hasAttr(compiled, 'recipient@example.com'));
324
- test.done();
325
- };
326
-
327
- module.exports['Search Compiler: CC'] = test => {
328
- let connection = createMockConnection();
329
- let compiled = searchCompiler(connection, { cc: 'cc@example.com' });
330
-
331
- test.ok(hasAttr(compiled, 'CC'));
332
- test.done();
333
- };
334
-
335
- module.exports['Search Compiler: BCC'] = test => {
336
- let connection = createMockConnection();
337
- let compiled = searchCompiler(connection, { bcc: 'bcc@example.com' });
338
-
339
- test.ok(hasAttr(compiled, 'BCC'));
340
- test.done();
341
- };
342
-
343
- module.exports['Search Compiler: SUBJECT'] = test => {
344
- let connection = createMockConnection();
345
- let compiled = searchCompiler(connection, { subject: 'Test Subject' });
346
-
347
- test.ok(hasAttr(compiled, 'SUBJECT'));
348
- test.ok(hasAttr(compiled, 'Test Subject'));
349
- test.done();
350
- };
351
-
352
- module.exports['Search Compiler: BODY'] = test => {
353
- let connection = createMockConnection();
354
- let compiled = searchCompiler(connection, { body: 'search text' });
355
-
356
- test.ok(hasAttr(compiled, 'BODY'));
357
- test.ok(hasAttr(compiled, 'search text'));
358
- test.done();
359
- };
360
-
361
- module.exports['Search Compiler: TEXT'] = test => {
362
- let connection = createMockConnection();
363
- let compiled = searchCompiler(connection, { text: 'full text search' });
364
-
365
- test.ok(hasAttr(compiled, 'TEXT'));
366
- test.ok(hasAttr(compiled, 'full text search'));
367
- test.done();
368
- };
369
-
370
- module.exports['Search Compiler: Text fields ignore falsy'] = test => {
371
- let connection = createMockConnection();
372
- let compiled = searchCompiler(connection, {
373
- from: '',
374
- to: null,
375
- subject: undefined
376
- });
377
-
378
- test.equal(compiled.length, 0);
379
- test.done();
380
- };
381
-
382
- // ============================================
383
- // UID tests
384
- // ============================================
385
-
386
- module.exports['Search Compiler: UID with string'] = test => {
387
- let connection = createMockConnection();
388
- let compiled = searchCompiler(connection, { uid: '1:*' });
389
-
390
- test.ok(hasAttr(compiled, 'UID'));
391
- let uidValueAttr = compiled.find(a => a.value === '1:*');
392
- test.equal(uidValueAttr.type, 'SEQUENCE');
393
- test.done();
394
- };
395
-
396
- module.exports['Search Compiler: UID with number'] = test => {
397
- let connection = createMockConnection();
398
- let compiled = searchCompiler(connection, { uid: 12345 });
399
-
400
- test.ok(hasAttr(compiled, 'UID'));
401
- test.ok(hasAttr(compiled, '12345'));
402
- test.done();
403
- };
404
-
405
- module.exports['Search Compiler: UID array compiles to a single comma-joined set'] = test => {
406
- let connection = createMockConnection();
407
- let compiled = searchCompiler(connection, { uid: [5, 7, 9] });
408
-
409
- // Separate tokens ("UID 5 7 9") would be parsed by the server as extra
410
- // sequence-number search keys ANDed to the query - the array means the set {5,7,9}
411
- test.ok(hasAttr(compiled, 'UID'));
412
- let uidValueAttr = compiled.find(a => a.value === '5,7,9');
413
- test.ok(uidValueAttr, 'array must be joined into one sequence set');
414
- test.equal(uidValueAttr.type, 'SEQUENCE');
415
- test.done();
416
- };
417
-
418
- module.exports['Search Compiler: UID accepts the SEARCHRES $ marker'] = test => {
419
- let connection = createMockConnection();
420
- let compiled = searchCompiler(connection, { uid: '$' });
421
-
422
- test.ok(hasAttr(compiled, 'UID'));
423
- let uidValueAttr = compiled.find(a => a.value === '$');
424
- test.ok(uidValueAttr, 'the saved-result marker must pass through');
425
- test.equal(uidValueAttr.type, 'SEQUENCE');
426
- test.done();
427
- };
428
-
429
- // ============================================
430
- // EMAILID / THREADID tests
431
- // ============================================
432
-
433
- module.exports['Search Compiler: EMAILID with OBJECTID'] = test => {
434
- let connection = createMockConnection({
435
- capabilities: [['OBJECTID', true]]
436
- });
437
- let compiled = searchCompiler(connection, { emailId: 'M1234567890' });
438
-
439
- test.ok(hasAttr(compiled, 'EMAILID'));
440
- test.ok(hasAttr(compiled, 'M1234567890'));
441
- test.done();
442
- };
443
-
444
- module.exports['Search Compiler: EMAILID falls back to X-GM-MSGID'] = test => {
445
- let connection = createMockConnection({
446
- capabilities: [['X-GM-EXT-1', true]]
447
- });
448
- let compiled = searchCompiler(connection, { emailId: '1234567890' });
449
-
450
- test.ok(hasAttr(compiled, 'X-GM-MSGID'));
451
- test.ok(hasAttr(compiled, '1234567890'));
452
- test.done();
453
- };
454
-
455
- module.exports['Search Compiler: EMAILID ignored without capability'] = test => {
456
- let connection = createMockConnection();
457
- let compiled = searchCompiler(connection, { emailId: '12345' });
458
-
459
- test.equal(compiled.length, 0);
460
- test.done();
461
- };
462
-
463
- module.exports['Search Compiler: THREADID with OBJECTID'] = test => {
464
- let connection = createMockConnection({
465
- capabilities: [['OBJECTID', true]]
466
- });
467
- let compiled = searchCompiler(connection, { threadId: 'T1234567890' });
468
-
469
- test.ok(hasAttr(compiled, 'THREADID'));
470
- test.ok(hasAttr(compiled, 'T1234567890'));
471
- test.done();
472
- };
473
-
474
- module.exports['Search Compiler: THREADID falls back to X-GM-THRID'] = test => {
475
- let connection = createMockConnection({
476
- capabilities: [['X-GM-EXT-1', true]]
477
- });
478
- let compiled = searchCompiler(connection, { threadId: '9876543210' });
479
-
480
- test.ok(hasAttr(compiled, 'X-GM-THRID'));
481
- test.ok(hasAttr(compiled, '9876543210'));
482
- test.done();
483
- };
484
-
485
- // ============================================
486
- // Gmail raw search tests
487
- // ============================================
488
-
489
- module.exports['Search Compiler: GMRAW with X-GM-EXT-1'] = test => {
490
- let connection = createMockConnection({
491
- capabilities: [['X-GM-EXT-1', true]]
492
- });
493
- let compiled = searchCompiler(connection, { gmraw: 'in:inbox is:unread' });
494
-
495
- test.ok(hasAttr(compiled, 'X-GM-RAW'));
496
- test.ok(hasAttr(compiled, 'in:inbox is:unread'));
497
- test.done();
498
- };
499
-
500
- module.exports['Search Compiler: GMAILRAW alias'] = test => {
501
- let connection = createMockConnection({
502
- capabilities: [['X-GM-EXT-1', true]]
503
- });
504
- let compiled = searchCompiler(connection, { gmailraw: 'has:attachment' });
505
-
506
- test.ok(hasAttr(compiled, 'X-GM-RAW'));
507
- test.ok(hasAttr(compiled, 'has:attachment'));
508
- test.done();
509
- };
510
-
511
- module.exports['Search Compiler: GMRAW throws without capability'] = test => {
512
- let connection = createMockConnection();
513
-
514
- try {
515
- searchCompiler(connection, { gmraw: 'test' });
516
- test.ok(false, 'Should have thrown');
517
- } catch (err) {
518
- test.equal(err.code, 'MissingServerExtension');
519
- test.ok(err.message.includes('X-GM-EXT-1'));
520
- }
521
-
522
- test.done();
523
- };
524
-
525
- // ============================================
526
- // Gmail label search tests
527
- // ============================================
528
-
529
- module.exports['Search Compiler: LABELS has compiles to label: via X-GM-RAW'] = test => {
530
- let connection = createMockConnection({
531
- capabilities: [['X-GM-EXT-1', true]]
532
- });
533
- let compiled = searchCompiler(connection, { labels: { has: ['Horizon'] } });
534
-
535
- test.ok(hasAttr(compiled, 'X-GM-RAW'));
536
- test.ok(hasAttr(compiled, 'label:Horizon'));
537
- test.done();
538
- };
539
-
540
- module.exports['Search Compiler: LABELS not compiles to -label: via X-GM-RAW'] = test => {
541
- let connection = createMockConnection({
542
- capabilities: [['X-GM-EXT-1', true]]
543
- });
544
- let compiled = searchCompiler(connection, { labels: { not: ['Horizon'] } });
545
-
546
- test.ok(hasAttr(compiled, 'X-GM-RAW'));
547
- test.ok(hasAttr(compiled, '-label:Horizon'));
548
- test.done();
549
- };
550
-
551
- module.exports['Search Compiler: LABELS with a non-object value is ignored'] = test => {
552
- let connection = createMockConnection();
553
-
554
- // Neither a falsy value nor a plain string is a { has, not } filter - both
555
- // must compile to nothing even without the Gmail extension
556
- let compiledNull = searchCompiler(connection, { labels: null });
557
- test.equal(compiledNull.length, 0);
558
-
559
- let compiledString = searchCompiler(connection, { labels: 'Horizon' });
560
- test.equal(compiledString.length, 0);
561
- test.done();
562
- };
563
-
564
- module.exports['Search Compiler: LABELS has and not combined'] = test => {
565
- let connection = createMockConnection({
566
- capabilities: [['X-GM-EXT-1', true]]
567
- });
568
- let compiled = searchCompiler(connection, { labels: { has: ['Imported'], not: ['Horizon'] } });
569
-
570
- test.ok(hasAttr(compiled, 'label:Imported -label:Horizon'));
571
- test.done();
572
- };
573
-
574
- module.exports['Search Compiler: LABELS quotes multi-word names'] = test => {
575
- let connection = createMockConnection({
576
- capabilities: [['X-GM-EXT-1', true]]
577
- });
578
- let compiled = searchCompiler(connection, { labels: { has: ['Some Label'] } });
579
-
580
- test.ok(hasAttr(compiled, 'label:"Some Label"'));
581
- test.done();
582
- };
583
-
584
- module.exports['Search Compiler: LABELS coexists with gmraw'] = test => {
585
- let connection = createMockConnection({
586
- capabilities: [['X-GM-EXT-1', true]]
587
- });
588
- let compiled = searchCompiler(connection, { gmraw: 'has:attachment', labels: { not: ['Horizon'] } });
589
-
590
- test.ok(hasAttr(compiled, 'has:attachment'));
591
- test.ok(hasAttr(compiled, '-label:Horizon'));
592
- test.done();
593
- };
594
-
595
- module.exports['Search Compiler: LABELS throws without capability'] = test => {
596
- let connection = createMockConnection();
597
-
598
- try {
599
- searchCompiler(connection, { labels: { not: ['Horizon'] } });
600
- test.ok(false, 'Should have thrown');
601
- } catch (err) {
602
- test.equal(err.code, 'MissingServerExtension');
603
- test.ok(err.message.includes('X-GM-EXT-1'));
604
- }
605
-
606
- test.done();
607
- };
608
-
609
- module.exports['Search Compiler: empty LABELS is a no-op without capability'] = test => {
610
- let connection = createMockConnection();
611
- let compiled = searchCompiler(connection, { labels: {} });
612
-
613
- test.ok(!hasAttr(compiled, 'X-GM-RAW'));
614
- test.equal(compiled.length, 0);
615
- test.done();
616
- };
617
-
618
- // ============================================
619
- // Date search tests
620
- // ============================================
621
-
622
- module.exports['Search Compiler: SINCE'] = test => {
623
- let connection = createMockConnection();
624
- let compiled = searchCompiler(connection, { since: new Date('2023-06-15') });
625
-
626
- test.ok(hasAttr(compiled, 'SINCE'));
627
- test.ok(hasAttr(compiled, '15-Jun-2023'));
628
- test.done();
629
- };
630
-
631
- module.exports['Search Compiler: BEFORE'] = test => {
632
- let connection = createMockConnection();
633
- let compiled = searchCompiler(connection, { before: new Date('2023-06-15T00:00:00.000Z') });
634
-
635
- test.ok(hasAttr(compiled, 'BEFORE'));
636
- test.done();
637
- };
638
-
639
- module.exports['Search Compiler: BEFORE with non-midnight time adjusts date'] = test => {
640
- let connection = createMockConnection();
641
- // Non-midnight time should advance to next day
642
- let compiled = searchCompiler(connection, { before: new Date('2023-06-15T12:30:00.000Z') });
643
-
644
- test.ok(hasAttr(compiled, 'BEFORE'));
645
- // Should be 16-Jun-2023 (next day)
646
- test.ok(hasAttr(compiled, '16-Jun-2023'));
647
- test.done();
648
- };
649
-
650
- module.exports['Search Compiler: ON'] = test => {
651
- let connection = createMockConnection();
652
- let compiled = searchCompiler(connection, { on: new Date('2023-06-15') });
653
-
654
- test.ok(hasAttr(compiled, 'ON'));
655
- test.done();
656
- };
657
-
658
- module.exports['Search Compiler: SENTBEFORE'] = test => {
659
- let connection = createMockConnection();
660
- let compiled = searchCompiler(connection, { sentbefore: new Date('2023-06-15T00:00:00.000Z') });
661
-
662
- test.ok(hasAttr(compiled, 'SENTBEFORE'));
663
- test.done();
664
- };
665
-
666
- module.exports['Search Compiler: SENTON'] = test => {
667
- let connection = createMockConnection();
668
- let compiled = searchCompiler(connection, { senton: new Date('2023-06-15') });
669
-
670
- test.ok(hasAttr(compiled, 'SENTON'));
671
- test.done();
672
- };
673
-
674
- module.exports['Search Compiler: SENTSINCE'] = test => {
675
- let connection = createMockConnection();
676
- let compiled = searchCompiler(connection, { sentsince: new Date('2023-06-15') });
677
-
678
- test.ok(hasAttr(compiled, 'SENTSINCE'));
679
- test.done();
680
- };
681
-
682
- module.exports['Search Compiler: SINCE with WITHIN extension'] = test => {
683
- let connection = createMockConnection({
684
- capabilities: [['WITHIN', true]]
685
- });
686
- let recentDate = new Date(Date.now() - 3600 * 1000); // 1 hour ago
687
- let compiled = searchCompiler(connection, { since: recentDate });
688
-
689
- test.ok(hasAttr(compiled, 'YOUNGER'));
690
- test.done();
691
- };
692
-
693
- module.exports['Search Compiler: BEFORE with WITHIN extension'] = test => {
694
- let connection = createMockConnection({
695
- capabilities: [['WITHIN', true]]
696
- });
697
- let oldDate = new Date(Date.now() - 86400 * 1000); // 1 day ago
698
- let compiled = searchCompiler(connection, { before: oldDate });
699
-
700
- test.ok(hasAttr(compiled, 'OLDER'));
701
- test.done();
702
- };
703
-
704
- module.exports['Search Compiler: Date with invalid value ignored'] = test => {
705
- let connection = createMockConnection();
706
- let compiled = searchCompiler(connection, { since: 'invalid-date' });
707
-
708
- // formatDate returns undefined for invalid dates
709
- test.equal(compiled.length, 0);
710
- test.done();
711
- };
712
-
713
- module.exports['Search Compiler: all date keys ignore an invalid Date object'] = test => {
714
- // An invalid Date is still a Date, so a plain Date brand check let it through:
715
- // BEFORE/SENTBEFORE threw RangeError from toISOString(), and with WITHIN
716
- // advertised BEFORE/SINCE compiled a literal "OLDER NaN"/"YOUNGER NaN" token
717
- for (let capabilities of [[['IMAP4rev1', true]], [['WITHIN', true]]]) {
718
- for (let key of ['before', 'since', 'on', 'sentBefore', 'sentOn', 'sentSince']) {
719
- let connection = createMockConnection({ capabilities });
720
- let compiled = searchCompiler(connection, { [key]: new Date('not-a-date') });
721
-
722
- test.deepEqual(compiled, [], `${key} should compile to no attributes`);
723
- }
724
- }
725
-
726
- test.done();
727
- };
728
-
729
- module.exports['Search Compiler: invalid date drops only its own criterion'] = test => {
730
- let connection = createMockConnection();
731
- let compiled = searchCompiler(connection, { seen: true, before: new Date('not-a-date') });
732
-
733
- test.deepEqual(compiled, [{ type: 'ATOM', value: 'SEEN' }]);
734
- test.done();
735
- };
736
-
737
- module.exports['Search Compiler: date string and Date object compile alike for BEFORE'] = test => {
738
- let asDate = searchCompiler(createMockConnection(), { before: new Date('2023-06-15T12:30:00.000Z') });
739
- let asString = searchCompiler(createMockConnection(), { before: '2023-06-15T12:30:00.000Z' });
740
-
741
- // Both input forms are documented, so both must get the +24h shift that makes
742
- // same-day BEFORE+SINCE ranges match. The string form used to skip it.
743
- test.deepEqual(asString, asDate);
744
- test.ok(hasAttr(asString, '16-Jun-2023'));
745
- test.done();
746
- };
747
-
748
- module.exports['Search Compiler: date string and Date object compile alike for WITHIN'] = test => {
749
- let connection = createMockConnection({ capabilities: [['WITHIN', true]] });
750
- let recentDate = new Date(Date.now() - 3600 * 1000); // 1 hour ago
751
- let asDate = searchCompiler(connection, { since: recentDate });
752
- let asString = searchCompiler(connection, { since: recentDate.toISOString() });
753
-
754
- // The string form used to skip the WITHIN shortcut and compile SINCE instead
755
- test.ok(hasAttr(asDate, 'YOUNGER'));
756
- test.ok(hasAttr(asString, 'YOUNGER'));
757
- // The keyword atom is followed by its value token
758
- let seconds = attrs => Number(attrs[1].value);
759
- // Both are measured against Date.now() at compile time, so allow a small drift
760
- test.ok(Math.abs(seconds(asString) - seconds(asDate)) <= 1);
761
- test.done();
762
- };
763
-
764
- // ============================================
765
- // KEYWORD tests
766
- // ============================================
767
-
768
- module.exports['Search Compiler: KEYWORD'] = test => {
769
- let connection = createMockConnection();
770
- let compiled = searchCompiler(connection, { keyword: '$CustomFlag' });
771
-
772
- test.ok(hasAttr(compiled, 'KEYWORD'));
773
- test.ok(hasAttr(compiled, '$CustomFlag'));
774
- test.done();
775
- };
776
-
777
- module.exports['Search Compiler: UNKEYWORD'] = test => {
778
- let connection = createMockConnection();
779
- let compiled = searchCompiler(connection, { unkeyword: '$CustomFlag' });
780
-
781
- test.ok(hasAttr(compiled, 'UNKEYWORD'));
782
- test.done();
783
- };
784
-
785
- module.exports['Search Compiler: KEYWORD with standard flag'] = test => {
786
- let connection = createMockConnection();
787
- let compiled = searchCompiler(connection, { keyword: '\\Seen' });
788
-
789
- test.ok(hasAttr(compiled, 'KEYWORD'));
790
- test.ok(hasAttr(compiled, '\\Seen'));
791
- test.done();
792
- };
793
-
794
- // ============================================
795
- // HEADER tests
796
- // ============================================
797
-
798
- module.exports['Search Compiler: HEADER with value'] = test => {
799
- let connection = createMockConnection();
800
- let compiled = searchCompiler(connection, {
801
- header: {
802
- 'X-Custom-Header': 'custom-value'
803
- }
804
- });
805
-
806
- test.ok(hasAttr(compiled, 'HEADER'));
807
- test.ok(hasAttr(compiled, 'X-CUSTOM-HEADER'));
808
- test.ok(hasAttr(compiled, 'custom-value'));
809
- test.done();
810
- };
811
-
812
- module.exports['Search Compiler: HEADER existence check'] = test => {
813
- let connection = createMockConnection();
814
- let compiled = searchCompiler(connection, {
815
- header: {
816
- 'X-Priority': true // Check header exists
817
- }
818
- });
819
-
820
- test.ok(hasAttr(compiled, 'HEADER'));
821
- test.ok(hasAttr(compiled, 'X-PRIORITY'));
822
- test.ok(hasAttr(compiled, '')); // Empty string for existence check
823
- test.done();
824
- };
825
-
826
- module.exports['Search Compiler: HEADER multiple headers'] = test => {
827
- let connection = createMockConnection();
828
- let compiled = searchCompiler(connection, {
829
- header: {
830
- 'X-Mailer': 'Outlook',
831
- 'X-Priority': '1'
832
- }
833
- });
834
-
835
- test.ok(hasAttr(compiled, 'X-MAILER'));
836
- test.ok(hasAttr(compiled, 'X-PRIORITY'));
837
- test.done();
838
- };
839
-
840
- module.exports['Search Compiler: HEADER ignores non-string values'] = test => {
841
- let connection = createMockConnection();
842
- let compiled = searchCompiler(connection, {
843
- header: {
844
- 'X-Number': 123,
845
- 'X-Null': null
846
- }
847
- });
848
-
849
- // Non-string values (except true) should be skipped
850
- test.ok(!hasAttr(compiled, 'X-NUMBER'));
851
- test.ok(!hasAttr(compiled, 'X-NULL'));
852
- test.done();
853
- };
854
-
855
- module.exports['Search Compiler: HEADER with null/invalid object'] = test => {
856
- let connection = createMockConnection();
857
-
858
- let compiled1 = searchCompiler(connection, { header: null });
859
- test.equal(compiled1.length, 0);
860
-
861
- let compiled2 = searchCompiler(connection, { header: 'not-an-object' });
862
- test.equal(compiled2.length, 0);
863
-
864
- test.done();
865
- };
866
-
867
- // ============================================
868
- // NOT operator tests
869
- // ============================================
870
-
871
- module.exports['Search Compiler: NOT operator'] = test => {
872
- let connection = createMockConnection();
873
- let compiled = searchCompiler(connection, {
874
- not: { from: 'spam@example.com' }
875
- });
876
-
877
- test.ok(hasAttr(compiled, 'NOT'));
878
- test.ok(hasAttr(compiled, 'FROM'));
879
- test.ok(hasAttr(compiled, 'spam@example.com'));
880
- test.done();
881
- };
882
-
883
- module.exports['Search Compiler: NOT with nested conditions'] = test => {
884
- let connection = createMockConnection();
885
- let compiled = searchCompiler(connection, {
886
- not: {
887
- seen: true,
888
- from: 'test@example.com'
889
- }
890
- });
891
-
892
- test.ok(hasAttr(compiled, 'NOT'));
893
- test.ok(hasAttr(compiled, 'SEEN'));
894
- test.ok(hasAttr(compiled, 'FROM'));
895
- // Compound NOT conditions should be wrapped in a sub-array (parenthesized)
896
- // so the server treats them as a single search-key
897
- test.equal(compiled[0].value, 'NOT');
898
- test.ok(Array.isArray(compiled[1]), 'compound NOT should be parenthesized');
899
- test.done();
900
- };
901
-
902
- module.exports['Search Compiler: NOT ignored when falsy'] = test => {
903
- let connection = createMockConnection();
904
-
905
- let compiled1 = searchCompiler(connection, { not: null });
906
- test.equal(compiled1.length, 0);
907
-
908
- let compiled2 = searchCompiler(connection, { not: false });
909
- test.equal(compiled2.length, 0);
910
-
911
- test.done();
912
- };
913
-
914
- // ============================================
915
- // OR operator tests
916
- // ============================================
917
-
918
- module.exports['Search Compiler: OR with two conditions'] = test => {
919
- let connection = createMockConnection();
920
- let compiled = searchCompiler(connection, {
921
- or: [{ from: 'alice@example.com' }, { from: 'bob@example.com' }]
922
- });
923
-
924
- test.ok(hasAttr(compiled, 'OR'));
925
- test.ok(hasAttr(compiled, 'FROM'));
926
- test.ok(hasAttr(compiled, 'alice@example.com'));
927
- test.ok(hasAttr(compiled, 'bob@example.com'));
928
- test.done();
929
- };
930
-
931
- module.exports['Search Compiler: OR with single condition'] = test => {
932
- let connection = createMockConnection();
933
- let compiled = searchCompiler(connection, {
934
- or: [{ from: 'only@example.com' }]
935
- });
936
-
937
- // Single condition should not add OR
938
- test.ok(!hasAttr(compiled, 'OR'));
939
- test.ok(hasAttr(compiled, 'FROM'));
940
- test.ok(hasAttr(compiled, 'only@example.com'));
941
- test.done();
942
- };
943
-
944
- module.exports['Search Compiler: OR with three conditions'] = test => {
945
- let connection = createMockConnection();
946
- let compiled = searchCompiler(connection, {
947
- or: [{ from: 'a@example.com' }, { from: 'b@example.com' }, { from: 'c@example.com' }]
948
- });
949
-
950
- // Should have OR for tree structure
951
- test.ok(hasAttr(compiled, 'OR'));
952
- test.ok(hasAttr(compiled, 'a@example.com'));
953
- test.ok(hasAttr(compiled, 'b@example.com'));
954
- test.ok(hasAttr(compiled, 'c@example.com'));
955
- test.done();
956
- };
957
-
958
- module.exports['Search Compiler: OR with four conditions'] = test => {
959
- let connection = createMockConnection();
960
- let compiled = searchCompiler(connection, {
961
- or: [{ from: 'a@example.com' }, { from: 'b@example.com' }, { from: 'c@example.com' }, { from: 'd@example.com' }]
962
- });
963
-
964
- test.ok(hasAttr(compiled, 'OR'));
965
- test.ok(hasAttr(compiled, 'a@example.com'));
966
- test.ok(hasAttr(compiled, 'd@example.com'));
967
- test.done();
968
- };
969
-
970
- module.exports['Search Compiler: OR ignored when empty'] = test => {
971
- let connection = createMockConnection();
972
-
973
- let compiled1 = searchCompiler(connection, { or: [] });
974
- test.equal(compiled1.length, 0);
975
-
976
- let compiled2 = searchCompiler(connection, { or: null });
977
- test.equal(compiled2.length, 0);
978
-
979
- let compiled3 = searchCompiler(connection, { or: 'not-an-array' });
980
- test.equal(compiled3.length, 0);
981
-
982
- test.done();
983
- };
984
-
985
- module.exports['Search Compiler: OR with null entry in array'] = test => {
986
- let connection = createMockConnection();
987
- let compiled = searchCompiler(connection, {
988
- or: [{ from: 'test@example.com' }, null]
989
- });
990
-
991
- // Should still process valid entries
992
- test.ok(hasAttr(compiled, 'FROM'));
993
- test.done();
994
- };
995
-
996
- // ============================================
997
- // Unicode / CHARSET tests
998
- // ============================================
999
-
1000
- module.exports['Search Compiler: Unicode adds CHARSET UTF-8'] = test => {
1001
- let connection = createMockConnection({
1002
- enabled: new Set() // UTF8=ACCEPT not enabled
1003
- });
1004
- let compiled = searchCompiler(connection, { from: 'test@example.com' });
1005
-
1006
- // No unicode, no charset
1007
- test.ok(!hasAttr(compiled, 'CHARSET'));
1008
-
1009
- // With unicode
1010
- let compiled2 = searchCompiler(connection, { subject: 'Test' });
1011
- test.ok(!hasAttr(compiled2, 'CHARSET'));
1012
-
1013
- test.done();
1014
- };
1015
-
1016
- module.exports['Search Compiler: Unicode in subject adds CHARSET'] = test => {
1017
- let connection = createMockConnection({
1018
- enabled: new Set() // UTF8=ACCEPT not enabled
1019
- });
1020
- let compiled = searchCompiler(connection, { subject: 'Test' });
1021
-
1022
- test.ok(!hasAttr(compiled, 'CHARSET'));
1023
- test.done();
1024
- };
1025
-
1026
- module.exports['Search Compiler: Unicode text triggers CHARSET'] = test => {
1027
- let connection = createMockConnection({
1028
- enabled: new Set() // UTF8=ACCEPT not enabled
1029
- });
1030
- let compiled = searchCompiler(connection, { from: 'user@example.com' });
1031
-
1032
- test.ok(!hasAttr(compiled, 'CHARSET'));
1033
- test.done();
1034
- };
1035
-
1036
- module.exports['Search Compiler: Unicode skipped when UTF8=ACCEPT enabled'] = test => {
1037
- let connection = createMockConnection({
1038
- enabled: new Set(['UTF8=ACCEPT'])
1039
- });
1040
- let compiled = searchCompiler(connection, { subject: 'Test' });
1041
-
1042
- test.ok(!hasAttr(compiled, 'CHARSET'));
1043
- test.done();
1044
- };
1045
-
1046
- module.exports['Search Compiler: Unicode on rev2 without UTF8=ACCEPT still adds CHARSET UTF-8'] = test => {
1047
- // RFC 9051 6.4.4: rev2 servers MUST assume UTF-8 when CHARSET is absent, and
1048
- // sending CHARSET UTF-8 is "redundant" but explicitly "permitted for improved
1049
- // compatibility" - pin the compiler's choice to send it until UTF8=ACCEPT is
1050
- // actually ENABLEd
1051
- let connection = createMockConnection({
1052
- capabilities: [['IMAP4rev2', true]],
1053
- enabled: new Set()
1054
- });
1055
- let compiled = searchCompiler(connection, { subject: 'Sõnum' });
1056
-
1057
- let charset = findAttr(compiled, 'CHARSET');
1058
- test.ok(charset, 'CHARSET prefix expected for a unicode search value');
1059
- test.ok(hasAttr(compiled, 'UTF-8'));
1060
- test.done();
1061
- };
1062
-
1063
- module.exports['Search Compiler: GMRAW with Unicode adds CHARSET'] = test => {
1064
- let connection = createMockConnection({
1065
- capabilities: [['X-GM-EXT-1', true]],
1066
- enabled: new Set()
1067
- });
1068
- let compiled = searchCompiler(connection, { gmraw: 'test query' });
1069
-
1070
- // ASCII query, no charset needed
1071
- test.ok(!hasAttr(compiled, 'CHARSET'));
1072
- test.done();
1073
- };
1074
-
1075
- module.exports['Search Compiler: HEADER with Unicode adds CHARSET'] = test => {
1076
- let connection = createMockConnection({
1077
- enabled: new Set()
1078
- });
1079
- let compiled = searchCompiler(connection, {
1080
- header: { Subject: 'ASCII only' }
1081
- });
1082
-
1083
- test.ok(!hasAttr(compiled, 'CHARSET'));
1084
- test.done();
1085
- };
1086
-
1087
- module.exports['Search Compiler: non-ASCII text field adds CHARSET UTF-8'] = test => {
1088
- let connection = createMockConnection({
1089
- enabled: new Set() // UTF8=ACCEPT not enabled
1090
- });
1091
- // Non-ASCII subject must flip hasUnicode and prepend CHARSET UTF-8
1092
- let compiled = searchCompiler(connection, { subject: 'résumé café' });
1093
- test.ok(hasAttr(compiled, 'CHARSET'));
1094
- test.ok(hasAttr(compiled, 'UTF-8'));
1095
- test.done();
1096
- };
1097
-
1098
- module.exports['Search Compiler: non-ASCII body field adds CHARSET UTF-8'] = test => {
1099
- let connection = createMockConnection({
1100
- enabled: new Set()
1101
- });
1102
- let compiled = searchCompiler(connection, { body: 'Grüße' });
1103
- test.ok(hasAttr(compiled, 'CHARSET'));
1104
- test.done();
1105
- };
1106
-
1107
- module.exports['Search Compiler: non-ASCII GMRAW adds CHARSET UTF-8'] = test => {
1108
- let connection = createMockConnection({
1109
- capabilities: [['X-GM-EXT-1', true]],
1110
- enabled: new Set()
1111
- });
1112
- let compiled = searchCompiler(connection, { gmraw: 'subject:café' });
1113
- test.ok(hasAttr(compiled, 'CHARSET'));
1114
- test.ok(hasAttr(compiled, 'X-GM-RAW'));
1115
- test.done();
1116
- };
1117
-
1118
- module.exports['Search Compiler: non-ASCII LABELS adds CHARSET UTF-8'] = test => {
1119
- let connection = createMockConnection({
1120
- capabilities: [['X-GM-EXT-1', true]],
1121
- enabled: new Set()
1122
- });
1123
- // The label filter compiles into an X-GM-RAW query, and a non-ASCII label name
1124
- // must mark the compiled query as Unicode the same way a direct gmraw value does
1125
- let compiled = searchCompiler(connection, { labels: { has: ['Tähtis'] } });
1126
- test.ok(hasAttr(compiled, 'CHARSET'));
1127
- test.ok(hasAttr(compiled, 'X-GM-RAW'));
1128
- test.ok(hasAttr(compiled, 'label:Tähtis'));
1129
- test.done();
1130
- };
1131
-
1132
- // ============================================
1133
- // Complex query tests
1134
- // ============================================
1135
-
1136
- module.exports['Search Compiler: Complex combined query'] = test => {
1137
- let connection = createMockConnection();
1138
- let compiled = searchCompiler(connection, {
1139
- seen: false,
1140
- from: 'sender@example.com',
1141
- since: new Date('2023-01-01'),
1142
- larger: 1000
1143
- });
1144
-
1145
- test.ok(hasAttr(compiled, 'UNSEEN'));
1146
- test.ok(hasAttr(compiled, 'FROM'));
1147
- test.ok(hasAttr(compiled, 'SINCE'));
1148
- test.ok(hasAttr(compiled, 'LARGER'));
1149
- test.done();
1150
- };
1151
-
1152
- module.exports['Search Compiler: OR combined with other criteria'] = test => {
1153
- let connection = createMockConnection();
1154
- let compiled = searchCompiler(connection, {
1155
- seen: true,
1156
- or: [{ from: 'a@example.com' }, { from: 'b@example.com' }]
1157
- });
1158
-
1159
- test.ok(hasAttr(compiled, 'SEEN'));
1160
- test.ok(hasAttr(compiled, 'OR'));
1161
- test.done();
1162
- };
1163
-
1164
- // ============================================
1165
- // OR tree structure tests
1166
- // ============================================
1167
-
1168
- module.exports['Search Compiler: OR with 3 conditions builds binary tree'] = test => {
1169
- let connection = createMockConnection();
1170
- let compiled = searchCompiler(connection, {
1171
- or: [{ from: 'alice' }, { to: 'bob' }, { subject: 'test' }]
1172
- });
1173
-
1174
- test.ok(hasAttr(compiled, 'OR'), 'should contain OR atom');
1175
- test.ok(hasAttr(compiled, 'alice'), 'should contain alice');
1176
- test.ok(hasAttr(compiled, 'bob'), 'should contain bob');
1177
- test.ok(hasAttr(compiled, 'test'), 'should contain test');
1178
- test.ok(hasAttr(compiled, 'FROM'), 'should contain FROM');
1179
- test.ok(hasAttr(compiled, 'TO'), 'should contain TO');
1180
- test.ok(hasAttr(compiled, 'SUBJECT'), 'should contain SUBJECT');
1181
- test.done();
1182
- };
1183
-
1184
- module.exports['Search Compiler: OR with 5 conditions produces 4 OR atoms'] = test => {
1185
- let connection = createMockConnection();
1186
- let compiled = searchCompiler(connection, {
1187
- or: [{ from: 'a' }, { from: 'b' }, { from: 'c' }, { from: 'd' }, { from: 'e' }]
1188
- });
1189
-
1190
- // 5 conditions need 4 OR atoms in a binary tree structure
1191
- let orCount = compiled.filter(a => a.value === 'OR').length;
1192
- test.equal(orCount, 4, 'should have exactly 4 OR atoms for 5 conditions');
1193
- test.ok(hasAttr(compiled, 'a'), 'should contain a');
1194
- test.ok(hasAttr(compiled, 'b'), 'should contain b');
1195
- test.ok(hasAttr(compiled, 'c'), 'should contain c');
1196
- test.ok(hasAttr(compiled, 'd'), 'should contain d');
1197
- test.ok(hasAttr(compiled, 'e'), 'should contain e');
1198
- test.done();
1199
- };
1200
-
1201
- module.exports['Search Compiler: OR with compound conditions wraps in parentheses'] = test => {
1202
- let connection = createMockConnection();
1203
- let compiled = searchCompiler(connection, {
1204
- or: [
1205
- { to: 'a@example.com', from: 'b@example.com' },
1206
- { to: 'c@example.com', from: 'd@example.com' }
1207
- ]
1208
- });
1209
-
1210
- // OR should be present
1211
- test.equal(compiled[0].value, 'OR');
1212
- // Each compound condition should be a sub-array (parenthesized)
1213
- test.ok(Array.isArray(compiled[1]), 'first compound operand should be parenthesized');
1214
- test.ok(Array.isArray(compiled[2]), 'second compound operand should be parenthesized');
1215
- // Check contents of parenthesized groups
1216
- test.ok(hasAttr(compiled[1], 'TO'), 'first group should have TO');
1217
- test.ok(hasAttr(compiled[1], 'FROM'), 'first group should have FROM');
1218
- test.ok(hasAttr(compiled[2], 'TO'), 'second group should have TO');
1219
- test.ok(hasAttr(compiled[2], 'FROM'), 'second group should have FROM');
1220
- test.done();
1221
- };
1222
-
1223
- module.exports['Search Compiler: OR with single-key conditions stays flat'] = test => {
1224
- let connection = createMockConnection();
1225
- let compiled = searchCompiler(connection, {
1226
- or: [{ from: 'a@example.com' }, { from: 'b@example.com' }]
1227
- });
1228
-
1229
- // Single-key conditions should not be wrapped in sub-arrays
1230
- test.equal(compiled[0].value, 'OR');
1231
- test.ok(!Array.isArray(compiled[1]), 'single-key operand should not be parenthesized');
1232
- test.equal(compiled[1].value, 'FROM');
1233
- test.done();
1234
- };
1235
-
1236
- module.exports['Search Compiler: OR with 5 compound conditions from issue #106'] = test => {
1237
- let connection = createMockConnection();
1238
- let compiled = searchCompiler(connection, {
1239
- or: [
1240
- { to: 'myemail@domain.com', from: '@anotherdomain.com' },
1241
- { to: 'myemail@domain.com', from: '@aseconddomain.com' },
1242
- { to: 'myemail@domain.com', from: '@athirddomain.fr' },
1243
- { to: 'anotheremail@domain.com', from: '@anotherdomain.fr' },
1244
- { to: 'anotheremail@domain.com', from: '@aseconddomain.com' }
1245
- ]
1246
- });
1247
-
1248
- // 5 conditions need 4 OR atoms
1249
- let orCount = compiled.filter(a => a.value === 'OR').length;
1250
- test.equal(orCount, 4, 'should have exactly 4 OR atoms for 5 conditions');
1251
-
1252
- // All compound conditions should be parenthesized (sub-arrays)
1253
- let subArrays = compiled.filter(a => Array.isArray(a));
1254
- test.equal(subArrays.length, 5, 'should have 5 parenthesized groups');
1255
-
1256
- // Each group should contain both TO and FROM
1257
- subArrays.forEach((group, i) => {
1258
- test.ok(hasAttr(group, 'TO'), 'group ' + i + ' should have TO');
1259
- test.ok(hasAttr(group, 'FROM'), 'group ' + i + ' should have FROM');
1260
- });
1261
-
1262
- test.done();
1263
- };
1264
-
1265
- // ============================================
1266
- // Unicode CHARSET in HEADER searches
1267
- // ============================================
1268
-
1269
- module.exports['Search Compiler: Unicode header search without UTF8=ACCEPT adds CHARSET'] = test => {
1270
- let connection = createMockConnection({
1271
- enabled: new Set()
1272
- });
1273
- let compiled = searchCompiler(connection, {
1274
- header: { subject: 'caf\u00e9' }
1275
- });
1276
-
1277
- test.ok(hasAttr(compiled, 'CHARSET'), 'should have CHARSET prefix');
1278
- test.ok(hasAttr(compiled, 'UTF-8'), 'should have UTF-8 value');
1279
- // CHARSET and UTF-8 should be the first two entries
1280
- test.equal(compiled[0].value, 'CHARSET', 'CHARSET should be first');
1281
- test.equal(compiled[1].value, 'UTF-8', 'UTF-8 should be second');
1282
- test.ok(hasAttr(compiled, 'HEADER'), 'should contain HEADER');
1283
- test.ok(hasAttr(compiled, 'caf\u00e9'), 'should contain the unicode value');
1284
- test.done();
1285
- };
1286
-
1287
- module.exports['Search Compiler: Unicode header search with UTF8=ACCEPT skips CHARSET'] = test => {
1288
- let connection = createMockConnection({
1289
- enabled: new Set(['UTF8=ACCEPT'])
1290
- });
1291
- let compiled = searchCompiler(connection, {
1292
- header: { subject: 'caf\u00e9' }
1293
- });
1294
-
1295
- test.ok(!hasAttr(compiled, 'CHARSET'), 'should NOT have CHARSET prefix');
1296
- test.ok(!hasAttr(compiled, 'UTF-8'), 'should NOT have UTF-8 value');
1297
- test.ok(hasAttr(compiled, 'HEADER'), 'should contain HEADER');
1298
- test.ok(hasAttr(compiled, 'caf\u00e9'), 'should contain the unicode value');
1299
- test.done();
1300
- };