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