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
package/lib/tools.js DELETED
@@ -1,1612 +0,0 @@
1
- /* eslint no-control-regex:0 */
2
-
3
- 'use strict';
4
-
5
- const libmime = require('libmime');
6
- const { resolveCharset } = require('./charsets');
7
- const { compiler } = require('./handler/imap-handler');
8
- const { createHash } = require('crypto');
9
- const { JPDecoder } = require('./jp-decoder');
10
- const iconv = require('iconv-lite');
11
-
12
- const FLAG_COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];
13
-
14
- // Error codes that only mean the connection is no longer usable. See logConnectionError().
15
- const CONNECTION_GONE_CODES = new Set(['NoConnection', 'EConnectionClosed', 'StateLogout']);
16
-
17
- // Upper bound for expanding server-supplied sequence ranges (see expandRange). 2^24
18
- // entries in total is far beyond any legitimate mailbox while keeping the worst-case
19
- // expansion of a hostile range set bounded.
20
- const EXPANDED_RANGE_LIMIT = 0x1000000;
21
-
22
- // Digit bounds for untrusted numeric values in server responses. UIDs, UIDVALIDITY and
23
- // message counts are 32-bit unsigned (nz-number in the RFC 9051 grammar, so at most 10
24
- // digits); MODSEQ and number64 values are 63-bit unsigned (RFC 7162, RFC 9051), at most
25
- // 19 digits. The bound is checked before BigInt()/Number(): a response line may carry up
26
- // to maxLineLength digits, and BigInt() on a multi-megabyte digit run costs hundreds of
27
- // milliseconds of non-yielding CPU.
28
- const MAX_UINT32_DIGITS = 10;
29
- const MAX_NUMBER64_DIGITS = 19;
30
-
31
- // Object keys that reach through the prototype chain when assigned to, or resolve to an
32
- // inherited member when read. Server-controlled strings become keys in several places
33
- // (STATUS items, QUOTA resources, BODYSTRUCTURE parameters, FETCH body part names), so they
34
- // all consult this one set rather than each carrying its own list.
35
- const UNSAFE_OBJECT_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
36
-
37
- // Extensions that RFC 9051 (IMAP4rev2) folds into the base protocol (Appendix E).
38
- // When IMAP4rev2 is active, these are available even without their own capability
39
- // token. BINARY is deliberately excluded - RFC 9051 only folds in the FETCH side,
40
- // which fetch.js handles with its own isRev2Active check, while the APPEND side
41
- // stays gated on the BINARY token. SPECIAL-USE is a partial fold: Appendix E only
42
- // folds in the special-use mailbox attributes, not the RFC 6154 LIST selection and
43
- // RETURN options - the only call sites that act on this entry are in list.js,
44
- // where a staged retry ladder recovers if a rev2-only server rejects the RETURN
45
- // option. The set mirrors the rest of the Appendix E list in full, including
46
- // entries no call site consults yet, so any future capability check gets the
47
- // rev2 folding for free.
48
- const IMAP4REV2_FOLDED_CAPABILITIES = new Set([
49
- 'ENABLE',
50
- 'ESEARCH',
51
- 'IDLE',
52
- 'LIST-EXTENDED',
53
- 'LIST-STATUS',
54
- 'LITERAL-',
55
- 'MOVE',
56
- 'NAMESPACE',
57
- 'SASL-IR',
58
- 'SEARCHRES',
59
- 'SPECIAL-USE',
60
- 'STATUS=SIZE',
61
- 'UIDPLUS',
62
- 'UNSELECT'
63
- ]);
64
-
65
- /**
66
- * Error subclass thrown when IMAP authentication fails.
67
- */
68
- class AuthenticationFailure extends Error {
69
- authenticationFailed = true;
70
- }
71
-
72
- // Deliberate no-op, used as the observer a guarded promise attaches to its own rejection.
73
- const noop = () => {};
74
-
75
- // The fields buildConnectionError() stamps to say *where* a connection error was rejected, as
76
- // opposed to what went wrong. restampConnectionError() clears them, because they belong to the
77
- // site that built the error rather than to the failure it describes.
78
- const CONNECTION_ERROR_SITE_KEYS = ['rejectedFrom', 'command', 'path'];
79
-
80
- const tools = {
81
- noop,
82
-
83
- /**
84
- * Builds an error describing a connection that is gone, stamped so it can be traced back to
85
- * where it came from: the connection id always travels on it, and each site names itself
86
- * through `meta` (`rejectedFrom`, plus the command or mailbox path it belongs to).
87
- *
88
- * A stack trace only records where an error was built, and close() hands a rejection to every
89
- * pending request and every queued lock in the same tick, so without these an error that
90
- * reaches a global unhandledRejection handler arrives with nothing that identifies the
91
- * connection it came from, let alone which of the rejected promises carried it.
92
- *
93
- * Takes the connection id rather than the connection, so the stamping stays in one place
94
- * without every caller having to be a full ImapFlow instance.
95
- *
96
- * @param {String} cid - Connection id
97
- * @param {String} code - Error code, e.g. 'NoConnection'
98
- * @param {String} message - Error message
99
- * @param {Object} [meta] - Fields to stamp on the error
100
- * @returns {Error} The stamped error
101
- */
102
- buildConnectionError(cid, code, message, meta) {
103
- const error = new Error(message);
104
- error.code = code;
105
- error.cid = cid;
106
- if (meta) {
107
- Object.assign(error, meta);
108
- }
109
- return error;
110
- },
111
-
112
- /**
113
- * Re-stamps an existing connection error for a different rejection site.
114
- *
115
- * The same failure can be handed to more than one promise - close() rejects the in-flight
116
- * command, and runIdle() then rejects everything queued behind it - and each of those is a
117
- * separate promise with a separate consumer. Sharing one error object reports whichever of
118
- * them escapes under the first site's marker, which is the attribution these markers exist to
119
- * give.
120
- *
121
- * Everything describing *what went wrong* is carried over, because a re-stamped error reaches
122
- * user code through run() and callers branch on `responseStatus`, `serverResponseCode` and
123
- * friends. Everything describing *where it was rejected* is dropped, because the new site owns
124
- * those and a leftover `command` from the previous site is exactly as misleading as a leftover
125
- * `rejectedFrom`. The original travels on as `cause`.
126
- *
127
- * @param {Error} err - The error being re-stamped
128
- * @param {Object} meta - Fields for the new site, e.g. { rejectedFrom: 'preCheckWaiter' }
129
- * @returns {Error} A separate error describing the same failure at the new site
130
- */
131
- restampConnectionError(err, meta) {
132
- let error = tools.buildConnectionError(err.cid, err.code, err.message, err);
133
- for (let key of CONNECTION_ERROR_SITE_KEYS) {
134
- delete error[key];
135
- }
136
- if (meta) {
137
- Object.assign(error, meta);
138
- }
139
- error.cause = err;
140
- return error;
141
- },
142
-
143
- /**
144
- * Creates a promise whose rejection is observed as soon as it exists.
145
- *
146
- * close() rejects every promise it owns - the in-flight and queued commands, the pending
147
- * connect(), the queued mailbox locks, the waiters for an IDLE break - synchronously, from a
148
- * socket event. A consumer that only reaches its `await` a microtask later has not attached a
149
- * handler yet at the moment Node decides whether the rejection was observed, and the whole
150
- * worker dies on the resulting unhandledRejection. The pre-attached observer settles that
151
- * question; the rejection still propagates normally to whoever awaits the returned promise.
152
- *
153
- * Creation and guarding are one call because splitting them is what actually goes wrong: the
154
- * guard was hand-attached at three of the four sites and the fourth (the IDLE-break waiter)
155
- * went unguarded, on exactly the path a server BYE takes.
156
- *
157
- * @param {Function} executor - Promise executor, (resolve, reject) => {}
158
- * @returns {Promise} The promise, with its rejection already observed
159
- */
160
- guardedPromise(executor) {
161
- let promise = new Promise(executor);
162
- promise.catch(noop);
163
- return promise;
164
- },
165
-
166
- /**
167
- * The already-rejected form of guardedPromise(), for a call that has to hand back a rejected
168
- * promise rather than throw.
169
- *
170
- * @param {Error} error - Rejection reason
171
- * @returns {Promise} Rejected promise, with its rejection already observed
172
- */
173
- guardedReject(error) {
174
- let promise = Promise.reject(error);
175
- promise.catch(noop);
176
- return promise;
177
- },
178
-
179
- /**
180
- * Detaches a background timer from the event loop, so it cannot keep the process alive on its
181
- * own. Applied to every background timer (auto-IDLE, IDLE restart, fallback polling, throttle
182
- * back-off, held-lock diagnostics); connection and greeting deadlines are deliberately left
183
- * attached, because a caller is waiting for connect() to settle.
184
- *
185
- * @param {Object} timer - Timer handle returned by setTimeout
186
- * @returns {Object} The same timer handle
187
- */
188
- unrefTimer(timer) {
189
- /* c8 ignore next 3 */ // node timers always expose unref(); the guard covers replaced globals in tests
190
- if (timer && typeof timer.unref === 'function') {
191
- timer.unref();
192
- }
193
- return timer;
194
- },
195
-
196
- /**
197
- * Logs a failure from background connection work at the level its cause deserves.
198
- *
199
- * Background work (IDLE sessions, polling timers, auto-IDLE) is interrupted by every normal
200
- * disconnect, so a rejection carrying one of the CONNECTION_GONE_CODES is expected rather
201
- * than notable and goes to debug. The three codes describe the same situation reached
202
- * through different guards: write() throws NoConnection or StateLogout, exec() rejects
203
- * EConnectionClosed for the window where the socket is destroyed but close() has not run
204
- * yet, and close() rejects pending requests with NoConnection.
205
- *
206
- * A connection error carrying `reason` is the exception. That field holds the server's
207
- * untagged BYE text ("Too many simultaneous connections", "Account is disabled"), which
208
- * serverBye() only records - this log call is the one place it becomes visible, and it is
209
- * usually the answer to why a client is reconnecting in a loop. Those stay at warn.
210
- *
211
- * Shared so the classification cannot drift between the call sites that make this decision.
212
- *
213
- * @param {Object} connection - IMAP connection instance
214
- * @param {String} msg - What failed, so the entries stay distinguishable in the log
215
- * @param {Error} err - The error to log
216
- */
217
- logConnectionError(connection, msg, err) {
218
- let routine = !!err && CONNECTION_GONE_CODES.has(err.code) && !err.reason;
219
- connection.log[routine ? 'debug' : 'warn']({ msg, err, cid: connection.id });
220
- },
221
-
222
- /**
223
- * Checks whether IMAP4rev2 semantics are active for the connection: either the
224
- * client enabled IMAP4rev2 explicitly, or the server is rev2-only (advertises
225
- * IMAP4rev2 without IMAP4rev1), in which case rev2 is the base protocol without
226
- * any ENABLE (RFC 9051 Appendix A). UTF-8 mailbox names apply in both cases.
227
- *
228
- * @param {Object} connection - IMAP connection instance
229
- * @returns {Boolean} True if IMAP4rev2 semantics apply to this session
230
- */
231
- isRev2Active(connection) {
232
- return connection.enabled.has('IMAP4REV2') || (connection.capabilities.has('IMAP4rev2') && !connection.capabilities.has('IMAP4rev1'));
233
- },
234
-
235
- /**
236
- * Checks a capability, accounting for extensions that RFC 9051 folds into base
237
- * IMAP4rev2. Falls back to the plain capability lookup on IMAP4rev1 sessions,
238
- * so behavior against rev1 servers is unchanged.
239
- *
240
- * @param {Object} connection - IMAP connection instance
241
- * @param {String} capability - Capability name, e.g. 'UIDPLUS'
242
- * @returns {Boolean} True if the capability (or its rev2-folded equivalent) is available
243
- */
244
- hasCapability(connection, capability) {
245
- if (connection.capabilities.has(capability)) {
246
- return true;
247
- }
248
- return IMAP4REV2_FOLDED_CAPABILITIES.has(capability) && tools.isRev2Active(connection);
249
- },
250
-
251
- /**
252
- * Builds the attribute list for a STATUS request - the standalone STATUS command
253
- * or the LIST-STATUS return option - from a status query object. Items the current
254
- * session cannot request (RECENT under IMAP4rev2, HIGHESTMODSEQ without CONDSTORE)
255
- * are silently dropped.
256
- *
257
- * @param {Object} connection - IMAP connection instance
258
- * @param {Object} statusQuery - Status data items to request, e.g. {messages: true}
259
- * @returns {Object[]} Attribute token list for the command compiler
260
- */
261
- buildStatusQueryAttributes(connection, statusQuery) {
262
- let attributes = [];
263
-
264
- Object.keys(statusQuery || {}).forEach(key => {
265
- if (!statusQuery[key]) {
266
- return;
267
- }
268
-
269
- switch (key.toUpperCase()) {
270
- case 'MESSAGES':
271
- case 'UIDNEXT':
272
- case 'UIDVALIDITY':
273
- case 'UNSEEN':
274
- attributes.push({ type: 'ATOM', value: key.toUpperCase() });
275
- break;
276
-
277
- case 'RECENT':
278
- // RECENT was removed in IMAP4rev2 (RFC 9051) - requesting it from a
279
- // rev2 session would get the whole STATUS request rejected
280
- if (!tools.isRev2Active(connection)) {
281
- attributes.push({ type: 'ATOM', value: key.toUpperCase() });
282
- }
283
- break;
284
-
285
- case 'HIGHESTMODSEQ':
286
- if (connection.capabilities.has('CONDSTORE')) {
287
- attributes.push({ type: 'ATOM', value: key.toUpperCase() });
288
- }
289
- break;
290
-
291
- case 'SIZE':
292
- // STATUS SIZE requires the STATUS=SIZE extension (RFC 8438), which
293
- // RFC 9051 folds into base IMAP4rev2
294
- if (tools.hasCapability(connection, 'STATUS=SIZE')) {
295
- attributes.push({ type: 'ATOM', value: key.toUpperCase() });
296
- }
297
- break;
298
-
299
- case 'DELETED':
300
- // STATUS DELETED is a base IMAP4rev2 addition (RFC 9051 Appendix E
301
- // item 3) with no standalone capability - requesting it from a plain
302
- // rev1 server would get the whole STATUS request rejected. RFC 9208
303
- // additionally makes it mandatory when QUOTA=RES-MESSAGE is advertised.
304
- if (tools.isRev2Active(connection) || connection.capabilities.has('QUOTA=RES-MESSAGE')) {
305
- attributes.push({ type: 'ATOM', value: key.toUpperCase() });
306
- }
307
- break;
308
- }
309
- });
310
-
311
- return attributes;
312
- },
313
-
314
- /**
315
- * Encodes a mailbox path to modified UTF-7 if the server does not support UTF8=ACCEPT.
316
- *
317
- * @param {Object} connection - IMAP connection instance
318
- * @param {String} path - Mailbox path to encode
319
- * @returns {String} Encoded mailbox path
320
- */
321
- encodePath(connection, path) {
322
- path = (path || '').toString();
323
- if (!connection.enabled.has('UTF8=ACCEPT') && !tools.isRev2Active(connection) && /[&\x00-\x08\x0b-\x0c\x0e-\x1f\u0080-\uffff]/.test(path)) {
324
- try {
325
- path = iconv.encode(path, 'utf-7-imap').toString();
326
- } catch {
327
- // ignore, keep name as is
328
- }
329
- }
330
- return path;
331
- },
332
-
333
- /**
334
- * Decodes a mailbox path from modified UTF-7 if the server does not support UTF8=ACCEPT.
335
- *
336
- * @param {Object} connection - IMAP connection instance
337
- * @param {String} path - Mailbox path to decode
338
- * @returns {String} Decoded mailbox path
339
- */
340
- decodePath(connection, path) {
341
- path = (path || '').toString();
342
- if (!connection.enabled.has('UTF8=ACCEPT') && !tools.isRev2Active(connection) && /[&]/.test(path)) {
343
- try {
344
- path = iconv.decode(Buffer.from(path), 'utf-7-imap').toString();
345
- } catch {
346
- // ignore, keep name as is
347
- }
348
- }
349
- return path;
350
- },
351
-
352
- /**
353
- * Normalizes a mailbox path by joining array segments with the namespace delimiter,
354
- * uppercasing INBOX, and prepending the namespace prefix if needed.
355
- *
356
- * @param {Object} connection - IMAP connection instance
357
- * @param {String|String[]} path - Mailbox path or array of path segments
358
- * @param {Boolean} [skipNamespace] - If true, skips prepending the namespace prefix
359
- * @returns {String} Normalized mailbox path
360
- */
361
- normalizePath(connection, path, skipNamespace) {
362
- if (Array.isArray(path)) {
363
- path = path.join((connection.namespace && connection.namespace.delimiter) || '');
364
- }
365
-
366
- if (path.toUpperCase() === 'INBOX') {
367
- // inbox is not case sensitive
368
- return 'INBOX';
369
- }
370
-
371
- // ensure namespace prefix if needed
372
- if (!skipNamespace && connection.namespace && connection.namespace.prefix && !path.startsWith(connection.namespace.prefix)) {
373
- path = connection.namespace.prefix + path;
374
- }
375
-
376
- return path;
377
- },
378
-
379
- /**
380
- * Compares two mailbox paths for equality after normalization.
381
- *
382
- * @param {Object} connection - IMAP connection instance
383
- * @param {String} a - First mailbox path
384
- * @param {String} b - Second mailbox path
385
- * @returns {Boolean} True if the paths are equal after normalization
386
- */
387
- comparePaths(connection, a, b) {
388
- if (!a || !b) {
389
- return false;
390
- }
391
- return tools.normalizePath(connection, a) === tools.normalizePath(connection, b);
392
- },
393
-
394
- /**
395
- * Parses a capability response list into a Map of capability names to values.
396
- *
397
- * @param {Array} list - Array of capability objects from IMAP response
398
- * @returns {Map<string, boolean|number>} Map of capability names to `true` or numeric values
399
- */
400
- updateCapabilities(list) {
401
- let map = new Map();
402
-
403
- if (list && Array.isArray(list)) {
404
- list.forEach(val => {
405
- // any entry can be a parsed NIL
406
- if (!val || typeof val.value !== 'string') {
407
- return;
408
- }
409
- let capability = val.value.toUpperCase().trim();
410
-
411
- if (capability === 'IMAP4REV1') {
412
- map.set('IMAP4rev1', true);
413
- return;
414
- }
415
-
416
- if (capability === 'IMAP4REV2') {
417
- map.set('IMAP4rev2', true);
418
- return;
419
- }
420
-
421
- if (capability.startsWith('APPENDLIMIT=')) {
422
- let splitPos = capability.indexOf('=');
423
- map.set('APPENDLIMIT', tools.parseUintValue(capability.substr(splitPos + 1)) || 0);
424
- return;
425
- }
426
-
427
- map.set(capability, true);
428
- });
429
- }
430
-
431
- return map;
432
- },
433
-
434
- AuthenticationFailure,
435
-
436
- /**
437
- * Extracts the IMAP response status code (e.g. AUTHENTICATIONFAILED, NONEXISTENT)
438
- * from a parsed server response.
439
- *
440
- * @param {Object} response - Parsed IMAP server response
441
- * @returns {String|false} Uppercase status code string, or false if not found
442
- */
443
- getStatusCode(response) {
444
- return response &&
445
- response.attributes &&
446
- response.attributes[0] &&
447
- response.attributes[0].section &&
448
- response.attributes[0].section[0] &&
449
- typeof response.attributes[0].section[0].value === 'string'
450
- ? response.attributes[0].section[0].value.toUpperCase().trim()
451
- : false;
452
- },
453
-
454
- /**
455
- * Compiles an IMAP response object back into a human-readable string.
456
- *
457
- * @param {Object} response - Parsed IMAP server response
458
- * @returns {Promise<String|false>} Compiled response text, or false if no response
459
- */
460
- async getErrorText(response) {
461
- if (!response) {
462
- return false;
463
- }
464
-
465
- try {
466
- return (await compiler(response)).toString();
467
- } catch {
468
- // The wire encoder refuses values that cannot be expressed as a valid IMAP
469
- // string, which is what keeps user-supplied data from breaking out of a
470
- // command. A server response is not held to that: the parser deliberately
471
- // tolerates stray bytes inside an OK/NO/BAD atom, and those bytes then have
472
- // no valid re-encoding. This text is diagnostic, so fall back to the logging
473
- // encoder rather than replacing the server's error with an encoding failure.
474
- return (await compiler(response, { isLogging: true })).toString();
475
- }
476
- },
477
-
478
- /**
479
- * Enhances an IMAP command error with the server response code and text.
480
- *
481
- * @param {Error} err - Error object with a `response` property
482
- * @returns {Promise<Error>} The enhanced error with `serverResponseCode` and string `response`
483
- */
484
- async enhanceCommandError(err) {
485
- let errorCode = tools.getStatusCode(err.response);
486
- if (errorCode) {
487
- err.serverResponseCode = errorCode;
488
- }
489
- err.response = await tools.getErrorText(err.response);
490
- return err;
491
- },
492
-
493
- /**
494
- * Converts a flat list of mailbox folders into a tree structure.
495
- *
496
- * @param {Object[]} folders - Array of folder objects from LIST/LSUB response
497
- * @returns {Object} Tree structure with a `root` flag and nested `folders` arrays
498
- */
499
- getFolderTree(folders) {
500
- let tree = {
501
- root: true,
502
- folders: []
503
- };
504
-
505
- let getTreeNode = parents => {
506
- let node = tree;
507
- if (!parents || !parents.length) {
508
- return node;
509
- }
510
-
511
- for (let parent of parents) {
512
- let cur = node.folders && node.folders.find(folder => folder.name === parent);
513
- if (cur) {
514
- node = cur;
515
- }
516
- }
517
-
518
- return node;
519
- };
520
-
521
- for (let folder of folders) {
522
- let parent = getTreeNode(folder.parent);
523
- // see if entry already exists
524
- let existing = parent.folders && parent.folders.find(existing => existing.name === folder.name);
525
- if (existing) {
526
- // update values
527
- existing.name = folder.name;
528
- existing.flags = folder.flags;
529
- existing.path = folder.path;
530
- existing.subscribed = !!folder.subscribed;
531
- existing.listed = !!folder.listed;
532
- existing.status = folder.status;
533
-
534
- if (folder.specialUse) {
535
- existing.specialUse = folder.specialUse;
536
- }
537
-
538
- if (folder.flags.has('\\Noselect')) {
539
- existing.disabled = true;
540
- }
541
- if (folder.flags.has('\\HasChildren') && !existing.folders) {
542
- existing.folders = [];
543
- }
544
- } else {
545
- // create new
546
- let data = {
547
- name: folder.name,
548
- flags: folder.flags,
549
- path: folder.path,
550
- subscribed: !!folder.subscribed,
551
- listed: !!folder.listed,
552
- status: folder.status
553
- };
554
-
555
- if (folder.delimiter) {
556
- data.delimiter = folder.delimiter;
557
- }
558
-
559
- if (folder.specialUse) {
560
- data.specialUse = folder.specialUse;
561
- }
562
-
563
- if (folder.flags.has('\\Noselect')) {
564
- data.disabled = true;
565
- }
566
-
567
- if (folder.flags.has('\\HasChildren')) {
568
- data.folders = [];
569
- }
570
-
571
- if (!parent.folders) {
572
- parent.folders = [];
573
- }
574
- parent.folders.push(data);
575
- }
576
- }
577
-
578
- return tree;
579
- },
580
-
581
- /**
582
- * Derives a flag color name from a message's flags Set using Apple Mail color flag rules.
583
- *
584
- * @param {Set<string>} flags - Message flags Set
585
- * @returns {String|null} Color name (e.g. 'red', 'orange') or null if not flagged
586
- */
587
- getFlagColor(flags) {
588
- if (!flags.has('\\Flagged')) {
589
- return null;
590
- }
591
-
592
- // Apple Mail encodes flag colors as a 3-bit value using $MailFlagBit0/1/2 keywords.
593
- // Bit 0 = 1, Bit 1 = 2, Bit 2 = 4. The resulting integer (0-6) indexes into FLAG_COLORS:
594
- // 0=red, 1=orange, 2=yellow, 3=green, 4=blue, 5=purple, 6=grey.
595
- // Value 7 (all bits set) is unused; defaults to red.
596
- const bit0 = flags.has('$MailFlagBit0') ? 1 : 0;
597
- const bit1 = flags.has('$MailFlagBit1') ? 2 : 0;
598
- const bit2 = flags.has('$MailFlagBit2') ? 4 : 0;
599
-
600
- const color = bit0 | bit1 | bit2; // eslint-disable-line no-bitwise
601
-
602
- return FLAG_COLORS[color] ?? 'red'; // default to red for the unused \b111
603
- },
604
-
605
- /**
606
- * Converts a color name to the corresponding flag add/remove operations for Apple Mail color flags.
607
- *
608
- * @param {String} color - Color name (e.g. 'red', 'orange', 'yellow')
609
- * @returns {Object|null} Object with `add` and `remove` arrays of flag strings, or null if invalid color
610
- */
611
- getColorFlags(color) {
612
- // Reverse mapping from a color name to the Apple Mail $MailFlagBit0/1/2 flags.
613
- // Returns an object with 'add' and 'remove' arrays so the caller can STORE +FLAGS/-FLAGS.
614
- const colorCode = color ? FLAG_COLORS.indexOf(color.toString().toLowerCase().trim()) : null;
615
- if (colorCode === null || colorCode < 0) {
616
- if (colorCode === null) {
617
- // Remove color: remove \Flagged and all MailFlagBit flags
618
- return { add: [], remove: ['\\Flagged', '$MailFlagBit0', '$MailFlagBit1', '$MailFlagBit2'] };
619
- }
620
- return null;
621
- }
622
-
623
- // Decompose color index back into its 3-bit representation
624
- let result = { add: ['\\Flagged'], remove: [] };
625
- for (let i = 0; i < 3; i++) {
626
- // eslint-disable-next-line no-bitwise
627
- if (colorCode & (1 << i)) {
628
- result.add.push(`$MailFlagBit${i}`);
629
- } else {
630
- result.remove.push(`$MailFlagBit${i}`);
631
- }
632
- }
633
- return result;
634
- },
635
-
636
- /**
637
- * Formats a raw untagged FETCH response into a structured message object.
638
- *
639
- * @param {Object} untagged - Parsed untagged IMAP response
640
- * @param {Object} mailbox - Current mailbox state object
641
- * @returns {Promise<Object>} Formatted message object with properties like seq, uid, flags, envelope, etc.
642
- */
643
- async formatMessageResponse(untagged, mailbox) {
644
- let map = {};
645
-
646
- // The sequence number indexes into mailbox state, so an unusable one is dropped rather
647
- // than coerced to NaN or Infinity
648
- map.seq = tools.parseUintValue(untagged.command, MAX_UINT32_DIGITS) || undefined;
649
-
650
- let key;
651
- let attributes = (untagged.attributes && untagged.attributes[1]) || [];
652
- for (let i = 0, len = attributes.length; i < len; i++) {
653
- let attribute = attributes[i];
654
- if (i % 2 === 0) {
655
- key = (
656
- await compiler({
657
- attributes: [attribute]
658
- })
659
- )
660
- .toString()
661
- .toLowerCase()
662
- .replace(/<\d+(\.\d+)?>$/, '');
663
- continue;
664
- }
665
- /* c8 ignore start */ // defensive: key is always a string produced by the compiler above
666
- if (typeof key !== 'string') {
667
- // should not happen
668
- continue;
669
- }
670
- /* c8 ignore stop */
671
-
672
- let getString = attribute => {
673
- if (!attribute) {
674
- return false;
675
- }
676
- if (typeof attribute.value === 'string') {
677
- return attribute.value;
678
- }
679
- if (Buffer.isBuffer(attribute.value)) {
680
- return attribute.value.toString();
681
- }
682
- };
683
-
684
- let getBuffer = attribute => {
685
- if (!attribute) {
686
- return false;
687
- }
688
- if (Buffer.isBuffer(attribute.value)) {
689
- return attribute.value;
690
- }
691
- };
692
-
693
- // NIL (parsed as null) and other non-array values yield an empty array, so callers
694
- // can safely index into the result. RFC 8474 allows e.g. `THREADID NIL` when the
695
- // server has no thread relation to report.
696
- let getArray = attribute => tools.getStringList(attribute);
697
-
698
- // Counts, sizes and UIDs are written into mailbox state and into range
699
- // computations, so only a bounded decimal run is usable - see parseUintValue().
700
- let getUint = (attribute, maxDigits) => tools.parseUintValue(getString(attribute), maxDigits);
701
-
702
- switch (key) {
703
- case 'body[]':
704
- case 'binary[]':
705
- map.source = getBuffer(attribute);
706
- break;
707
-
708
- case 'uid':
709
- // A UID feeds mailbox.uidNext one line below, and from there every range
710
- // computation, so an unusable one is dropped rather than coerced
711
- map.uid = getUint(attribute, MAX_UINT32_DIGITS) || undefined;
712
- // If the UID we just saw is >= the mailbox's uidNext, bump uidNext.
713
- // This keeps the local uidNext estimate current without requiring a
714
- // separate STATUS command, handling cases where new messages arrived
715
- // since the last SELECT/EXAMINE.
716
- if (map.uid && (!mailbox.uidNext || mailbox.uidNext <= map.uid)) {
717
- mailbox.uidNext = map.uid + 1;
718
- }
719
- break;
720
-
721
- case 'modseq': {
722
- // BigInt() throws on a non-numeric or missing value, and the throw
723
- // drops the whole message from the result set - so a malformed
724
- // MODSEQ from the server must be skipped, not surfaced.
725
- let modseq = tools.parseBigIntValue(getArray(attribute)[0]);
726
- if (modseq === false) {
727
- break;
728
- }
729
- map.modseq = modseq;
730
- // Similarly, keep the local highestModseq estimate up to date.
731
- // This is critical for CONDSTORE/QRESYNC delta syncing.
732
- if (map.modseq && (!mailbox.highestModseq || mailbox.highestModseq < map.modseq)) {
733
- mailbox.highestModseq = map.modseq;
734
- }
735
- break;
736
- }
737
-
738
- case 'emailid':
739
- // OBJECTID extension (RFC 8474): server-assigned stable email identifier
740
- map.emailId = getArray(attribute)[0];
741
- break;
742
-
743
- case 'x-gm-msgid':
744
- // Gmail extension: X-GM-MSGID is Gmail's unique message ID.
745
- // Mapped to the same emailId field as OBJECTID for a unified API,
746
- // but this is a Gmail-specific numeric string, not an RFC 8474 ObjectID.
747
- map.emailId = getString(attribute);
748
- break;
749
-
750
- case 'threadid':
751
- map.threadId = getArray(attribute)[0];
752
- break;
753
-
754
- case 'x-gm-thrid':
755
- map.threadId = getString(attribute);
756
- break;
757
-
758
- case 'x-gm-labels':
759
- map.labels = new Set(getArray(attribute));
760
- break;
761
-
762
- case 'rfc822.size':
763
- map.size = getUint(attribute) || 0;
764
- break;
765
-
766
- case 'flags':
767
- map.flags = new Set(getArray(attribute));
768
- break;
769
-
770
- case 'envelope':
771
- map.envelope = tools.parseEnvelope(attribute);
772
- break;
773
-
774
- case 'bodystructure':
775
- map.bodyStructure = tools.parseBodystructure(attribute);
776
- break;
777
-
778
- case 'internaldate': {
779
- let value = getString(attribute);
780
- let date = new Date(value);
781
- if (date.toString() === 'Invalid Date') {
782
- map.internalDate = value;
783
- } else {
784
- map.internalDate = date;
785
- }
786
- break;
787
- }
788
-
789
- default: {
790
- let match = key.match(/(body|binary)\[/i);
791
- if (match) {
792
- let partKey = key.replace(/^(body|binary)\[|]$/gi, '');
793
- partKey = partKey.replace(/\.fields.*$/g, '');
794
-
795
- let value = getBuffer(attribute);
796
- if (partKey === 'header') {
797
- map.headers = value;
798
- break;
799
- }
800
-
801
- if (!map.bodyParts) {
802
- map.bodyParts = new Map();
803
- }
804
- map.bodyParts.set(partKey, value);
805
-
806
- if (match[1].toLowerCase() === 'binary') {
807
- // The part arrived via FETCH BINARY (RFC 3516, FETCH side folded
808
- // into IMAP4rev2), so the server has already removed the
809
- // content-transfer-encoding - consumers must not decode it again.
810
- // Recorded from the actual response, not predicted from the
811
- // request, so it stays correct even if a server answers a BINARY
812
- // request with a BODY response or vice versa.
813
- if (!map.binaryParts) {
814
- map.binaryParts = new Set();
815
- }
816
- map.binaryParts.add(partKey);
817
- }
818
- break;
819
- }
820
- break;
821
- }
822
- }
823
- }
824
-
825
- if (map.emailId || map.uid) {
826
- // define account unique ID for this email
827
-
828
- // normalize path to use ascii, so we would always get the same ID
829
- let path = mailbox.path;
830
- if (/[\u0080-\uffff]/.test(path)) {
831
- try {
832
- path = iconv.encode(path, 'utf-7-imap').toString();
833
- } catch {
834
- // ignore
835
- }
836
- }
837
-
838
- // Non-cryptographic identifier: MD5 is used only to derive a stable, compact
839
- // account-unique id from non-secret data (path:uidValidity:uid). No security
840
- // property (collision/preimage resistance, secrecy) is relied upon, so a fast
841
- // hash is the appropriate choice here — not a security-sensitive use.
842
- map.id =
843
- map.emailId ||
844
- createHash('md5')
845
- .update([path, mailbox.uidValidity?.toString() || '', map.uid.toString()].join(':'))
846
- .digest('hex');
847
- }
848
-
849
- if (map.flags) {
850
- let flagColor = tools.getFlagColor(map.flags);
851
- if (flagColor) {
852
- map.flagColor = flagColor;
853
- }
854
- }
855
-
856
- return map;
857
- },
858
-
859
- /**
860
- * Strips surrounding double quotes from a name string.
861
- *
862
- * @param {String} name - Raw name string potentially wrapped in quotes
863
- * @returns {String} Name with surrounding quotes removed
864
- */
865
- processName(name) {
866
- name = (name || '').toString();
867
- if (name.length > 2 && name.at(0) === '"' && name.at(-1) === '"') {
868
- name = name.slice(1, -1);
869
- }
870
- return name;
871
- },
872
-
873
- /**
874
- * Decodes an ENVELOPE text field for display: encoded words first, then the
875
- * surrounding quotes some servers leave in place.
876
- *
877
- * @param {String} value - Raw field value from an ENVELOPE response
878
- * @returns {String} Decoded, unquoted text
879
- */
880
- decodeText(value) {
881
- return tools.processName(libmime.decodeWords(value));
882
- },
883
-
884
- /**
885
- * Parses a raw IMAP ENVELOPE response into a structured envelope object.
886
- *
887
- * @param {Array} entry - Raw envelope data array from IMAP response
888
- * @returns {Object} Parsed envelope with date, subject, from, to, cc, bcc, messageId, etc.
889
- */
890
- parseEnvelope(entry) {
891
- let getStrValue = obj => {
892
- if (!obj) {
893
- return false;
894
- }
895
- if (typeof obj.value === 'string') {
896
- return obj.value;
897
- }
898
- if (Buffer.isBuffer(obj.value)) {
899
- return obj.value.toString();
900
- }
901
- /* c8 ignore next */ // defensive: envelope tokens are always string/Buffer/NIL, never another type
902
- return obj.value;
903
- };
904
-
905
- let processAddresses = function (list) {
906
- /* c8 ignore next 2 */ // defensive: processAddresses is only called with non-empty arrays, so the [] fallback is unreachable
907
- return []
908
- .concat(list || [])
909
- .map(addr => {
910
- if (!addr) {
911
- // A NIL entry inside an address list: skip it instead of
912
- // throwing on the dereference and dropping the message
913
- return false;
914
- }
915
-
916
- let name = tools.decodeText(getStrValue(addr[0]));
917
- let mailbox = getStrValue(addr[2]) || '';
918
- let host = getStrValue(addr[3]) || '';
919
-
920
- if (!host) {
921
- // RFC 9051 7.5.2: a NIL host field marks RFC 5322 group syntax, it is not
922
- // an empty domain. A non-NIL mailbox then holds the group name phrase, a
923
- // NIL one closes the group. Joining the fields anyway would invent an
924
- // address that never appeared in the message, eg. "undisclosed-recipients@",
925
- // so surface the group name as a display name and leave the address empty.
926
- // End-of-group markers carry neither and the filter below drops them.
927
- // The mirror case, a NIL mailbox with a host, is left alone on purpose:
928
- // the grammar gives it no meaning, so a server sending it is simply
929
- // malformed rather than signalling anything we could act on.
930
- return { name: name || (mailbox && tools.decodeText(mailbox)), address: '' };
931
- }
932
-
933
- return { name, address: `${mailbox}@${host}` };
934
- })
935
- .filter(addr => addr && (addr.name || addr.address));
936
- },
937
- envelope = {};
938
-
939
- if (entry[0] && entry[0].value) {
940
- let date = new Date(getStrValue(entry[0]));
941
- if (date.toString() === 'Invalid Date') {
942
- envelope.date = getStrValue(entry[0]);
943
- } else {
944
- envelope.date = date;
945
- }
946
- }
947
-
948
- if (entry[1] && entry[1].value) {
949
- envelope.subject = libmime.decodeWords(getStrValue(entry[1]));
950
- }
951
-
952
- if (entry[2] && entry[2].length) {
953
- envelope.from = processAddresses(entry[2]);
954
- }
955
-
956
- if (entry[3] && entry[3].length) {
957
- envelope.sender = processAddresses(entry[3]);
958
- }
959
-
960
- if (entry[4] && entry[4].length) {
961
- envelope.replyTo = processAddresses(entry[4]);
962
- }
963
-
964
- if (entry[5] && entry[5].length) {
965
- envelope.to = processAddresses(entry[5]);
966
- }
967
-
968
- if (entry[6] && entry[6].length) {
969
- envelope.cc = processAddresses(entry[6]);
970
- }
971
-
972
- if (entry[7] && entry[7].length) {
973
- envelope.bcc = processAddresses(entry[7]);
974
- }
975
-
976
- if (entry[8] && entry[8].value) {
977
- /* c8 ignore next */ // the guard ensures getStrValue is truthy here, so the '' fallback is unreachable
978
- envelope.inReplyTo = (getStrValue(entry[8]) || '').toString().trim();
979
- }
980
-
981
- if (entry[9] && entry[9].value) {
982
- /* c8 ignore next */ // the guard ensures getStrValue is truthy here, so the '' fallback is unreachable
983
- envelope.messageId = (getStrValue(entry[9]) || '').toString().trim();
984
- }
985
-
986
- return envelope;
987
- },
988
-
989
- /**
990
- * Parses structured MIME parameter arrays (including RFC 2231 continuations)
991
- * into a flat key-value object.
992
- *
993
- * @param {Array} arr - Raw parameter array from BODYSTRUCTURE response
994
- * @returns {Object} Key-value object of decoded parameters
995
- */
996
- getStructuredParams(arr) {
997
- let key;
998
-
999
- let params = {};
1000
-
1001
- // BODYSTRUCTURE parameters come as flat key/value pairs: [key1, val1, key2, val2, ...]
1002
- [].concat(arr || []).forEach((val, j) => {
1003
- if (j % 2) {
1004
- // Parameter names are server-controlled. The load-bearing check is the one in
1005
- // the continuation pass below, where the value is an object; here the value is
1006
- // always a string, which the __proto__ setter ignores anyway.
1007
- if (!tools.isUnsafeKey(key)) {
1008
- params[key] = libmime.decodeWords(((val && val.value) || '').toString());
1009
- }
1010
- } else {
1011
- key = ((val && val.value) || '').toString().toLowerCase();
1012
- }
1013
- });
1014
-
1015
- // Detect RFC 2231 encoded filenames that were placed in the plain 'filename' param
1016
- // instead of 'filename*'. The pattern charset'language'encoded_value indicates encoding.
1017
- if (params.filename && !params['filename*'] && /^[a-z\-_0-9]+'[a-z]*'[^'\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]+/.test(params.filename)) {
1018
- // seems like encoded value
1019
- let [encoding, , encodedValue] = params.filename.split("'");
1020
- if (resolveCharset(encoding)) {
1021
- params['filename*'] = `${encoding}''${encodedValue}`;
1022
- }
1023
- }
1024
-
1025
- // RFC 2231 parameter continuations: parameters like filename*0, filename*1, etc.
1026
- // are split parts of a single value. Parameters ending with '*' contain charset info.
1027
- // This pass collects continuation parts and groups them by their base key name.
1028
- Object.keys(params).forEach(key => {
1029
- let actualKey;
1030
- let nr;
1031
- let value;
1032
-
1033
- // Match keys ending with *N or *N* (where N is the continuation index)
1034
- let match = key.match(/\*((\d+)\*?)?$/);
1035
-
1036
- if (!match) {
1037
- // nothing to do here, does not seem like a continuation param
1038
- return;
1039
- }
1040
-
1041
- actualKey = key.substr(0, match.index).toLowerCase();
1042
- nr = Number(match[2]) || 0;
1043
-
1044
- if (tools.isUnsafeKey(actualKey)) {
1045
- // A continuation key like "__proto__*0*" would group under "__proto__":
1046
- // params['__proto__'] resolves to Object.prototype, so the grouping
1047
- // writes below would mutate it (process-wide pollution). Drop the part.
1048
- delete params[key];
1049
- return;
1050
- }
1051
-
1052
- if (!params[actualKey] || typeof params[actualKey] !== 'object') {
1053
- params[actualKey] = {
1054
- charset: false,
1055
- values: []
1056
- };
1057
- }
1058
-
1059
- value = params[key];
1060
-
1061
- // The first segment (*0*) may contain charset and language: charset'language'value
1062
- if (nr === 0 && match[0].at(-1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
1063
- params[actualKey].charset = match[1] || 'utf-8';
1064
- value = match[2];
1065
- }
1066
-
1067
- params[actualKey].values.push({ nr, value });
1068
-
1069
- // remove the old reference
1070
- delete params[key];
1071
- });
1072
-
1073
- // Reassemble split RFC 2231 strings by sorting continuation parts and joining them.
1074
- // For charset-encoded values, convert URL-encoded (%XX) sequences to MIME quoted-printable
1075
- // format (=?charset?Q?...?=) so libmime.decodeWords can decode them to Unicode.
1076
- Object.keys(params).forEach(key => {
1077
- let value;
1078
- if (params[key] && Array.isArray(params[key].values)) {
1079
- value = params[key].values
1080
- .sort((a, b) => a.nr - b.nr)
1081
- .map(val => (val && val.value) || '')
1082
- .join('');
1083
-
1084
- if (params[key].charset) {
1085
- // Convert URL encoding (%AB) to MIME quoted-printable (=AB) by:
1086
- // 1. Escaping QP-special chars (=, ?, _, space) as %XX
1087
- // 2. Replacing all '%' with '=' to switch from URL encoding to QP encoding
1088
- // 3. Wrapping in =?charset?Q?...?= for libmime to decode
1089
- params[key] = libmime.decodeWords(
1090
- '=?' +
1091
- params[key].charset +
1092
- '?Q?' +
1093
- value
1094
- // fix invalidly encoded chars
1095
- .replace(/[=?_\s]/g, s => {
1096
- if (s === ' ') {
1097
- return '_';
1098
- }
1099
- let c = s.charCodeAt(0).toString(16);
1100
- return '%' + (c.length < 2 ? '0' : '') + c;
1101
- })
1102
- // change from urlencoding to percent encoding
1103
- .replace(/%/g, '=') +
1104
- '?='
1105
- );
1106
- } else {
1107
- params[key] = libmime.decodeWords(value);
1108
- }
1109
- }
1110
- });
1111
-
1112
- return params;
1113
- },
1114
-
1115
- /**
1116
- * Parses a raw IMAP BODYSTRUCTURE response into a structured tree of body parts.
1117
- *
1118
- * @param {Array} entry - Raw BODYSTRUCTURE data array from IMAP response
1119
- * @returns {Object} Parsed body structure tree with part numbers, types, parameters, and child nodes
1120
- */
1121
- parseBodystructure(entry) {
1122
- // Recursively walks the BODYSTRUCTURE tree, building MIME part numbers.
1123
- // Part numbers follow the IMAP dot-notation: "1", "1.1", "2.3", etc.
1124
- // The root multipart has no part number; its children start at 1.
1125
- let walk = (node, path) => {
1126
- path = path || [];
1127
-
1128
- let curNode = {},
1129
- i = 0,
1130
- part = 0;
1131
-
1132
- // Build the dot-separated part number from the path array (e.g., [1,2] -> "1.2")
1133
- if (path.length) {
1134
- curNode.part = path.join('.');
1135
- }
1136
-
1137
- // multipart: first elements are arrays (child body parts), followed by the subtype string
1138
- if (Array.isArray(node[0])) {
1139
- curNode.childNodes = [];
1140
- // Each child array is a nested body part; increment part counter for each
1141
- while (Array.isArray(node[i])) {
1142
- curNode.childNodes.push(walk(node[i], path.concat(++part)));
1143
- i++;
1144
- }
1145
-
1146
- // multipart type
1147
- curNode.type = 'multipart/' + ((node[i++] || {}).value || '').toString().toLowerCase();
1148
-
1149
- // extension data (not available for BODY requests)
1150
-
1151
- // body parameter parenthesized list
1152
- if (i < node.length - 1) {
1153
- if (node[i]) {
1154
- curNode.parameters = tools.getStructuredParams(node[i]);
1155
- }
1156
- i++;
1157
- }
1158
- } else {
1159
- // content type
1160
- curNode.type = [((node[i++] || {}).value || '').toString().toLowerCase(), ((node[i++] || {}).value || '').toString().toLowerCase()].join('/');
1161
-
1162
- // body parameter parenthesized list
1163
- if (node[i]) {
1164
- curNode.parameters = tools.getStructuredParams(node[i]);
1165
- }
1166
- i++;
1167
-
1168
- // id
1169
- if (node[i]) {
1170
- curNode.id = (node[i].value || '').toString();
1171
- }
1172
- i++;
1173
-
1174
- // description
1175
- if (node[i]) {
1176
- curNode.description = (node[i].value || '').toString();
1177
- }
1178
- i++;
1179
-
1180
- // encoding
1181
- if (node[i]) {
1182
- curNode.encoding = (node[i].value || '').toString().toLowerCase();
1183
- }
1184
- i++;
1185
-
1186
- // size
1187
- if (node[i]) {
1188
- curNode.size = Number(node[i].value || 0) || 0;
1189
- }
1190
- i++;
1191
-
1192
- if (curNode.type === 'message/rfc822') {
1193
- // message/rfc822 is special in IMAP BODYSTRUCTURE: after the standard
1194
- // 7 fields, it includes an embedded envelope, a nested bodystructure,
1195
- // and a line count for the encapsulated message.
1196
-
1197
- // envelope of the encapsulated message
1198
- if (node[i]) {
1199
- /* c8 ignore next */ // node[i] is truthy inside this guard, so the [] fallback is unreachable
1200
- curNode.envelope = tools.parseEnvelope([].concat(node[i] || []));
1201
- }
1202
- i++;
1203
-
1204
- if (node[i]) {
1205
- curNode.childNodes = [
1206
- // The nested bodystructure reuses the same path (not path+1) because
1207
- // the encapsulated message shares the part number with its wrapper.
1208
- // Distinction is via suffixes: path.MIME = wrapper headers,
1209
- // path.HEADER = encapsulated message headers.
1210
- walk(node[i], path)
1211
- ];
1212
- }
1213
- i++;
1214
-
1215
- // line count
1216
- if (node[i]) {
1217
- curNode.lineCount = Number(node[i].value || 0) || 0;
1218
- }
1219
- i++;
1220
- }
1221
-
1222
- if (/^text\//.test(curNode.type)) {
1223
- // Per RFC 3501, text/* parts include an additional line count field after size.
1224
- // However, some servers omit this field, producing 11 elements instead of 12+.
1225
-
1226
- // NB! some less known servers do not include the line count value
1227
- // length should be 12+
1228
- if (node.length === 11 && Array.isArray(node[i + 1]) && !Array.isArray(node[i + 2])) {
1229
- // invalid structure, disposition params are shifted -- skip the line count
1230
- } else {
1231
- // correct structure, line count number is provided
1232
- if (node[i]) {
1233
- curNode.lineCount = Number(node[i].value || 0) || 0;
1234
- }
1235
- i++;
1236
- }
1237
- }
1238
-
1239
- // extension data (not available for BODY requests)
1240
-
1241
- // md5
1242
- if (i < node.length - 1) {
1243
- if (node[i]) {
1244
- curNode.md5 = (node[i].value || '').toString().toLowerCase();
1245
- }
1246
- i++;
1247
- }
1248
- }
1249
-
1250
- // the following are shared extension values (for both multipart and non-multipart parts)
1251
- // not available for BODY requests
1252
-
1253
- // body disposition
1254
- if (i < node.length - 1) {
1255
- if (Array.isArray(node[i]) && node[i].length) {
1256
- curNode.disposition = ((node[i][0] && node[i][0].value) || '').toString().toLowerCase();
1257
- if (Array.isArray(node[i][1])) {
1258
- curNode.dispositionParameters = tools.getStructuredParams(node[i][1]);
1259
- }
1260
- }
1261
- i++;
1262
- }
1263
-
1264
- // body language
1265
- if (i < node.length - 1) {
1266
- if (node[i]) {
1267
- /* c8 ignore next */ // node[i] is truthy inside this guard, so the [] fallback is unreachable
1268
- curNode.language = [].concat(node[i] || []).map(val => ((val && val.value) || '').toString().toLowerCase());
1269
- }
1270
- i++;
1271
- }
1272
-
1273
- // body location
1274
- // NB! defined as a "string list" in RFC3501 but replaced in errata document with "string"
1275
- // Errata: http://www.rfc-editor.org/errata_search.php?rfc=3501
1276
- if (i < node.length - 1) {
1277
- if (node[i]) {
1278
- curNode.location = (node[i].value || '').toString();
1279
- }
1280
- }
1281
-
1282
- return curNode;
1283
- };
1284
-
1285
- return walk(entry);
1286
- },
1287
-
1288
- /**
1289
- * Checks if a value is a Date object.
1290
- *
1291
- * @param {*} obj - Value to check
1292
- * @returns {Boolean} True if the value is a Date object
1293
- */
1294
- isDate(obj) {
1295
- return Object.prototype.toString.call(obj) === '[object Date]';
1296
- },
1297
-
1298
- /**
1299
- * Converts a value to a valid Date object, or returns null.
1300
- *
1301
- * @param {*} value - Date object or date string to convert
1302
- * @returns {Date|null} Valid Date object, or null if conversion fails
1303
- */
1304
- toValidDate(value) {
1305
- if (!value) {
1306
- return null;
1307
- }
1308
- if (typeof value === 'string') {
1309
- value = new Date(value);
1310
- }
1311
- if (!tools.isDate(value) || value.toString() === 'Invalid Date') {
1312
- return null;
1313
- }
1314
- return value;
1315
- },
1316
-
1317
- /**
1318
- * Formats a date value into IMAP date format (DD-Mon-YYYY).
1319
- *
1320
- * @param {Date|String} value - Date to format
1321
- * @returns {String|undefined} Formatted date string, or undefined if invalid
1322
- */
1323
- formatDate(value) {
1324
- value = tools.toValidDate(value);
1325
- if (!value) {
1326
- return;
1327
- }
1328
-
1329
- let dateParts = value.toISOString().substr(0, 10).split('-');
1330
- dateParts.reverse();
1331
-
1332
- let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
1333
- dateParts[1] = months[Number(dateParts[1]) - 1];
1334
-
1335
- return dateParts.join('-');
1336
- },
1337
-
1338
- /**
1339
- * Formats a date value into IMAP date-time format (DD-Mon-YYYY HH:MM:SS +0000).
1340
- *
1341
- * @param {Date|String} value - Date to format
1342
- * @returns {String|undefined} Formatted date-time string, or undefined if invalid
1343
- */
1344
- formatDateTime(value) {
1345
- value = tools.toValidDate(value);
1346
- if (!value) {
1347
- return;
1348
- }
1349
-
1350
- let dateStr = tools.formatDate(value).replace(/^0/, ' '); //starts with date-day-fixed with leading 0 replaced by SP
1351
- let timeStr = value.toISOString().substr(11, 8);
1352
-
1353
- return `${dateStr} ${timeStr} +0000`;
1354
- },
1355
-
1356
- /**
1357
- * Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent),
1358
- * and capitalizes system flags properly.
1359
- *
1360
- * @param {String} flag - Flag string to normalize
1361
- * @returns {String|false} Normalized flag string, or false if the flag cannot be set
1362
- */
1363
- formatFlag(flag) {
1364
- switch (flag.toLowerCase()) {
1365
- case '\\recent':
1366
- // can not set or remove
1367
- return false;
1368
- case '\\seen':
1369
- case '\\answered':
1370
- case '\\flagged':
1371
- case '\\deleted':
1372
- case '\\draft':
1373
- // normalize capitalization (e.g., "\\seen" -> "\\Seen")
1374
- return flag.toLowerCase().replace(/^\\./, c => c.toUpperCase());
1375
- }
1376
- return flag;
1377
- },
1378
-
1379
- /**
1380
- * Checks if a flag can be used in the given mailbox based on permanent flags.
1381
- *
1382
- * @param {Object} mailbox - Mailbox object with permanentFlags
1383
- * @param {String} flag - Flag to check
1384
- * @returns {Boolean} True if the flag is allowed
1385
- */
1386
- canUseFlag(mailbox, flag) {
1387
- return !mailbox || !mailbox.permanentFlags || mailbox.permanentFlags.has('\\*') || mailbox.permanentFlags.has(flag);
1388
- },
1389
-
1390
- /**
1391
- * Checks that a value is a valid IMAP sequence number or UID: a non-zero
1392
- * 32-bit unsigned integer (nz-number in the RFC 9051 grammar). Guards range
1393
- * expansion against untrusted server input such as 'Infinity' or '0:*'.
1394
- *
1395
- * @param {Number} value - Value to check
1396
- * @returns {Boolean} True if the value is a valid sequence number/UID
1397
- */
1398
- isValidSequenceValue(value) {
1399
- return Number.isSafeInteger(value) && value > 0 && value <= 0xffffffff;
1400
- },
1401
-
1402
- /**
1403
- * Checks that an untrusted response value is a pure decimal digit run no longer than
1404
- * the given bound.
1405
- *
1406
- * `!isNaN(value)` is not usable for this: it also passes '1e5', ' 12 ', '0x10' and
1407
- * 'Infinity'. BigInt() throws on all of them and Number() silently returns a value the
1408
- * grammar never allowed, so both are wrong in a response handler that is only trying to
1409
- * read one field. The length bound is checked before the pattern so an arbitrarily long
1410
- * digit run is rejected without any conversion work.
1411
- *
1412
- * @param {*} value - Raw value from the response.
1413
- * @param {Number} maxDigits - Maximum number of digits accepted.
1414
- * @returns {Boolean} True if the value is a decimal string within the bound.
1415
- */
1416
- isDecimalString(value, maxDigits) {
1417
- return typeof value === 'string' && value.length > 0 && value.length <= maxDigits && /^[0-9]+$/.test(value);
1418
- },
1419
-
1420
- /**
1421
- * Checks whether a server-supplied string is unsafe to use as a key on a plain object.
1422
- * Assigning "__proto__" writes through the prototype setter instead of creating an own
1423
- * property, and reading "constructor" or "prototype" resolves to an inherited member.
1424
- *
1425
- * @param {*} key - Candidate key from a server response.
1426
- * @returns {Boolean} True if the key must not be used.
1427
- */
1428
- isUnsafeKey(key) {
1429
- return UNSAFE_OBJECT_KEYS.has(key);
1430
- },
1431
-
1432
- /**
1433
- * Reads a parsed attribute list of atoms or strings (a flag list, a capability list) into
1434
- * an array of strings. Any element can be a parsed NIL, and the list itself can be NIL,
1435
- * so both levels are guarded here rather than at each call site.
1436
- *
1437
- * @param {*} list - Parsed attribute list from a response.
1438
- * @returns {String[]} The string values, in order, with unusable entries dropped.
1439
- */
1440
- getStringList(list) {
1441
- if (!Array.isArray(list)) {
1442
- return [];
1443
- }
1444
- return list.map(entry => (entry && typeof entry.value === 'string' ? entry.value : false)).filter(entry => entry);
1445
- },
1446
-
1447
- /**
1448
- * Parses an untrusted decimal value from a server response into a BigInt.
1449
- *
1450
- * @param {*} value - Raw value from the response.
1451
- * @param {Number} [maxDigits=MAX_NUMBER64_DIGITS] - Maximum number of digits accepted.
1452
- * @returns {BigInt|false} The parsed value, or false when it is not usable.
1453
- */
1454
- parseBigIntValue(value, maxDigits) {
1455
- if (!tools.isDecimalString(value, maxDigits || MAX_NUMBER64_DIGITS)) {
1456
- return false;
1457
- }
1458
- return BigInt(value);
1459
- },
1460
-
1461
- /**
1462
- * Parses an untrusted decimal value from a server response into a Number. Values beyond
1463
- * the safe integer range are rejected rather than rounded: a silently rounded count or
1464
- * UID corrupts every range computation derived from it.
1465
- *
1466
- * @param {*} value - Raw value from the response.
1467
- * @param {Number} [maxDigits=MAX_NUMBER64_DIGITS] - Maximum number of digits accepted.
1468
- * @returns {Number|false} The parsed value, or false when it is not usable.
1469
- */
1470
- parseUintValue(value, maxDigits) {
1471
- if (!tools.isDecimalString(value, maxDigits || MAX_NUMBER64_DIGITS)) {
1472
- return false;
1473
- }
1474
- let num = Number(value);
1475
- return Number.isSafeInteger(num) ? num : false;
1476
- },
1477
-
1478
- /**
1479
- * Expands an IMAP sequence range string (e.g. "1:3,5,7:9") into an array of numbers.
1480
- *
1481
- * Entries with endpoints that are not valid nz-numbers are skipped - the input
1482
- * may come from an untrusted server, and 'Infinity' or similar garbage would
1483
- * otherwise loop without bound. The whole set is expanded to at most
1484
- * EXPANDED_RANGE_LIMIT entries in total: legitimate responses never reach the limit
1485
- * (the mailbox would need that many messages), while hostile input is cut off
1486
- * instead of exhausting memory. The total is capped, not just each range -
1487
- * otherwise "1:16777216,1:16777216,..." would multiply the per-range bound by an
1488
- * unbounded number of ranges.
1489
- *
1490
- * @param {String} range - IMAP sequence range string
1491
- * @returns {Number[]} Array of expanded sequence numbers
1492
- */
1493
- expandRange(range) {
1494
- let result = [];
1495
- // Callers pass whatever the response parser produced for the sequence set, and a
1496
- // malformed response can leave that as `false` (e.g. a VANISHED response carrying
1497
- // only the (EARLIER) tag). Nothing to expand then, and throwing here would abort
1498
- // the handler for the rest of the response.
1499
- if (typeof range !== 'string') {
1500
- return result;
1501
- }
1502
- for (let entry of range.split(',')) {
1503
- if (result.length >= EXPANDED_RANGE_LIMIT) {
1504
- break;
1505
- }
1506
- entry = entry.trim();
1507
- let colon = entry.indexOf(':');
1508
- if (colon < 0) {
1509
- let value = Number(entry);
1510
- if (tools.isValidSequenceValue(value)) {
1511
- result.push(value);
1512
- }
1513
- continue;
1514
- }
1515
- let first = Number(entry.substr(0, colon));
1516
- let second = Number(entry.substr(colon + 1));
1517
- if (!tools.isValidSequenceValue(first) || !tools.isValidSequenceValue(second)) {
1518
- continue;
1519
- }
1520
- if (first === second) {
1521
- result.push(first);
1522
- continue;
1523
- }
1524
- // Remaining total budget doubles as the per-range bound
1525
- let remaining = EXPANDED_RANGE_LIMIT - result.length;
1526
- if (first < second) {
1527
- let last = Math.min(second, first + remaining - 1);
1528
- for (let i = first; i <= last; i++) {
1529
- result.push(i);
1530
- }
1531
- } else {
1532
- let last = Math.max(second, first - remaining + 1);
1533
- for (let i = first; i >= last; i--) {
1534
- result.push(i);
1535
- }
1536
- }
1537
- }
1538
- return result;
1539
- },
1540
-
1541
- /**
1542
- * Returns a stream decoder for the given charset. Uses a special Japanese
1543
- * charset decoder for JIS/ISO-2022-JP, otherwise delegates to iconv-lite.
1544
- *
1545
- * @param {String} [charset='ascii'] - Character set name
1546
- * @param {Number} [maxBytes] - Bound for the bytes the decoder may buffer. Only
1547
- * relevant for the Japanese decoder, which must buffer its whole input before
1548
- * it can decode: without the bound a server could defeat a caller's maxBytes
1549
- * download limit simply by labelling the part with a Japanese charset.
1550
- * @returns {Object} A stream decoder (Transform stream) for the charset
1551
- */
1552
- getDecoder(charset, maxBytes) {
1553
- charset = (charset || 'ascii').toString().trim().toLowerCase();
1554
- if (/^jis|^iso-?2022-?jp|^euc-?jp/.test(charset)) {
1555
- // special case not supported by iconv-lite
1556
- return new JPDecoder(charset, maxBytes);
1557
- }
1558
-
1559
- return iconv.decodeStream(charset);
1560
- },
1561
-
1562
- /**
1563
- * Packs an array of message sequence numbers into a compact IMAP range string
1564
- * (e.g. [1,2,3,5,7,8] becomes "1:3,5,7:8").
1565
- *
1566
- * @param {Number|Number[]} list - Sequence number or array of sequence numbers
1567
- * @returns {String} Packed IMAP sequence range string
1568
- */
1569
- packMessageRange(list) {
1570
- if (!Array.isArray(list)) {
1571
- list = [].concat(list || []);
1572
- }
1573
-
1574
- if (!list.length) {
1575
- return '';
1576
- }
1577
-
1578
- // Deduplicate before sorting so that repeated values do not produce
1579
- // overlapping/non-canonical tokens (e.g. [1,1,2,3] -> "1:3", not "1,1:3").
1580
- list = Array.from(new Set(list)).sort((a, b) => a - b);
1581
-
1582
- let last = list[list.length - 1];
1583
- let result = [[last]];
1584
- for (let i = list.length - 2; i >= 0; i--) {
1585
- if (list[i] === list[i + 1] - 1) {
1586
- result[0].unshift(list[i]);
1587
- continue;
1588
- }
1589
- result.unshift([list[i]]);
1590
- }
1591
-
1592
- result = result.map(item => {
1593
- if (item.length === 1) {
1594
- return item[0];
1595
- }
1596
- return item.shift() + ':' + item.pop();
1597
- });
1598
-
1599
- return result.join(',');
1600
- }
1601
- };
1602
-
1603
- module.exports = tools;
1604
-
1605
- // Shared bound for expanding server-supplied sequence sets (see expandRange). Exported so
1606
- // other places that expand server sequences (e.g. ESEARCH ALL in commands/search.js) can
1607
- // apply the same absolute ceiling instead of inventing their own.
1608
- module.exports.EXPANDED_RANGE_LIMIT = EXPANDED_RANGE_LIMIT;
1609
-
1610
- // Exported so call sites can ask for the tighter 32-bit bound where the grammar requires it
1611
- // (UID, UIDVALIDITY, message counts) instead of the parsers' 63-bit default.
1612
- module.exports.MAX_UINT32_DIGITS = MAX_UINT32_DIGITS;