imapkit 4.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (485) hide show
  1. package/README.md +32 -19
  2. package/bin/imapkit.js +12 -16
  3. package/dist/cjs/addressparser.d.ts +21 -0
  4. package/dist/cjs/addressparser.js +277 -0
  5. package/dist/cjs/arguments.d.ts +31 -0
  6. package/{lib → dist/cjs}/arguments.js +9 -21
  7. package/dist/cjs/bodystructure.d.ts +25 -0
  8. package/dist/cjs/bodystructure.js +150 -0
  9. package/dist/cjs/cert.d.ts +2 -0
  10. package/dist/cjs/cert.js +7 -0
  11. package/dist/cjs/command-states.d.ts +36 -0
  12. package/dist/cjs/command-states.js +102 -0
  13. package/dist/cjs/commands/append.d.ts +88 -0
  14. package/dist/cjs/commands/append.js +299 -0
  15. package/dist/cjs/commands/capability.d.ts +2 -0
  16. package/dist/cjs/commands/capability.js +44 -0
  17. package/dist/cjs/commands/check.d.ts +2 -0
  18. package/dist/cjs/commands/check.js +28 -0
  19. package/dist/cjs/commands/close.d.ts +2 -0
  20. package/dist/cjs/commands/close.js +35 -0
  21. package/dist/cjs/commands/copy.d.ts +30 -0
  22. package/dist/cjs/commands/copy.js +107 -0
  23. package/dist/cjs/commands/create.d.ts +2 -0
  24. package/dist/cjs/commands/create.js +53 -0
  25. package/dist/cjs/commands/delete.d.ts +2 -0
  26. package/dist/cjs/commands/delete.js +63 -0
  27. package/dist/cjs/commands/examine.d.ts +2 -0
  28. package/dist/cjs/commands/examine.js +19 -0
  29. package/dist/cjs/commands/expunge.d.ts +2 -0
  30. package/dist/cjs/commands/expunge.js +32 -0
  31. package/dist/cjs/commands/fetch.d.ts +23 -0
  32. package/dist/cjs/commands/fetch.js +208 -0
  33. package/dist/cjs/commands/handlers/fetch.d.ts +12 -0
  34. package/dist/cjs/commands/handlers/fetch.js +195 -0
  35. package/dist/cjs/commands/handlers/flags.d.ts +17 -0
  36. package/dist/cjs/commands/handlers/flags.js +39 -0
  37. package/dist/cjs/commands/handlers/search.d.ts +85 -0
  38. package/dist/cjs/commands/handlers/search.js +504 -0
  39. package/dist/cjs/commands/handlers/status.d.ts +34 -0
  40. package/dist/cjs/commands/handlers/status.js +81 -0
  41. package/dist/cjs/commands/handlers/store.d.ts +3 -0
  42. package/dist/cjs/commands/handlers/store.js +117 -0
  43. package/dist/cjs/commands/index.d.ts +2 -0
  44. package/dist/cjs/commands/index.js +61 -0
  45. package/dist/cjs/commands/list.d.ts +2 -0
  46. package/dist/cjs/commands/list.js +86 -0
  47. package/dist/cjs/commands/login.d.ts +2 -0
  48. package/dist/cjs/commands/login.js +62 -0
  49. package/dist/cjs/commands/logout.d.ts +2 -0
  50. package/dist/cjs/commands/logout.js +40 -0
  51. package/dist/cjs/commands/lsub.d.ts +2 -0
  52. package/dist/cjs/commands/lsub.js +79 -0
  53. package/dist/cjs/commands/noop.d.ts +2 -0
  54. package/dist/cjs/commands/noop.js +28 -0
  55. package/dist/cjs/commands/rename.d.ts +2 -0
  56. package/dist/cjs/commands/rename.js +98 -0
  57. package/dist/cjs/commands/search.d.ts +10 -0
  58. package/dist/cjs/commands/search.js +71 -0
  59. package/dist/cjs/commands/select.d.ts +19 -0
  60. package/dist/cjs/commands/select.js +230 -0
  61. package/dist/cjs/commands/status.d.ts +2 -0
  62. package/dist/cjs/commands/status.js +57 -0
  63. package/dist/cjs/commands/store.d.ts +10 -0
  64. package/dist/cjs/commands/store.js +134 -0
  65. package/dist/cjs/commands/subscribe.d.ts +2 -0
  66. package/dist/cjs/commands/subscribe.js +51 -0
  67. package/dist/cjs/commands/uid-copy.d.ts +2 -0
  68. package/dist/cjs/commands/uid-copy.js +19 -0
  69. package/dist/cjs/commands/uid-fetch.d.ts +3 -0
  70. package/dist/cjs/commands/uid-fetch.js +17 -0
  71. package/dist/cjs/commands/uid-search.d.ts +3 -0
  72. package/dist/cjs/commands/uid-search.js +17 -0
  73. package/dist/cjs/commands/uid-store.d.ts +3 -0
  74. package/dist/cjs/commands/uid-store.js +17 -0
  75. package/dist/cjs/commands/unsubscribe.d.ts +2 -0
  76. package/dist/cjs/commands/unsubscribe.js +50 -0
  77. package/dist/cjs/dates.d.ts +64 -0
  78. package/dist/cjs/dates.js +124 -0
  79. package/dist/cjs/deflate-layer.d.ts +83 -0
  80. package/dist/cjs/deflate-layer.js +232 -0
  81. package/dist/cjs/encoded-words.d.ts +15 -0
  82. package/dist/cjs/encoded-words.js +85 -0
  83. package/dist/cjs/envelope.d.ts +33 -0
  84. package/dist/cjs/envelope.js +86 -0
  85. package/dist/cjs/esearch.d.ts +96 -0
  86. package/dist/cjs/esearch.js +190 -0
  87. package/dist/cjs/framing.d.ts +66 -0
  88. package/{lib → dist/cjs}/framing.js +9 -26
  89. package/dist/cjs/index.d.ts +9 -0
  90. package/dist/cjs/index.js +56 -0
  91. package/dist/cjs/list-extensions.d.ts +50 -0
  92. package/dist/cjs/list-extensions.js +35 -0
  93. package/dist/cjs/load-plugins.d.ts +21 -0
  94. package/dist/cjs/load-plugins.js +89 -0
  95. package/dist/cjs/mailbox-name.d.ts +26 -0
  96. package/dist/cjs/mailbox-name.js +127 -0
  97. package/dist/cjs/mimeparser.d.ts +137 -0
  98. package/dist/cjs/mimeparser.js +727 -0
  99. package/dist/cjs/mock-client.d.ts +39 -0
  100. package/dist/cjs/mock-client.js +236 -0
  101. package/dist/cjs/numbers.d.ts +28 -0
  102. package/dist/cjs/numbers.js +52 -0
  103. package/dist/cjs/package.json +3 -0
  104. package/dist/cjs/plugins/acl.d.ts +2 -0
  105. package/dist/cjs/plugins/acl.js +888 -0
  106. package/dist/cjs/plugins/appendlimit.d.ts +16 -0
  107. package/dist/cjs/plugins/appendlimit.js +87 -0
  108. package/dist/cjs/plugins/auth-plain.d.ts +2 -0
  109. package/dist/cjs/plugins/auth-plain.js +89 -0
  110. package/dist/cjs/plugins/binary.d.ts +30 -0
  111. package/dist/cjs/plugins/binary.js +247 -0
  112. package/dist/cjs/plugins/catenate.d.ts +14 -0
  113. package/dist/cjs/plugins/catenate.js +250 -0
  114. package/dist/cjs/plugins/compress.d.ts +7 -0
  115. package/dist/cjs/plugins/compress.js +79 -0
  116. package/dist/cjs/plugins/condstore.d.ts +5 -0
  117. package/dist/cjs/plugins/condstore.js +492 -0
  118. package/dist/cjs/plugins/context-search.d.ts +8 -0
  119. package/dist/cjs/plugins/context-search.js +304 -0
  120. package/dist/cjs/plugins/context-sort.d.ts +6 -0
  121. package/dist/cjs/plugins/context-sort.js +32 -0
  122. package/dist/cjs/plugins/create-special-use.d.ts +7 -0
  123. package/dist/cjs/plugins/create-special-use.js +101 -0
  124. package/dist/cjs/plugins/enable.d.ts +23 -0
  125. package/dist/cjs/plugins/enable.js +127 -0
  126. package/dist/cjs/plugins/esearch.d.ts +6 -0
  127. package/dist/cjs/plugins/esearch.js +142 -0
  128. package/dist/cjs/plugins/esort.d.ts +6 -0
  129. package/dist/cjs/plugins/esort.js +60 -0
  130. package/dist/cjs/plugins/id.d.ts +5 -0
  131. package/dist/cjs/plugins/id.js +123 -0
  132. package/dist/cjs/plugins/idle.d.ts +5 -0
  133. package/dist/cjs/plugins/idle.js +85 -0
  134. package/dist/cjs/plugins/imap4rev2.d.ts +6 -0
  135. package/dist/cjs/plugins/imap4rev2.js +184 -0
  136. package/dist/cjs/plugins/index.d.ts +2 -0
  137. package/dist/cjs/plugins/index.js +119 -0
  138. package/dist/cjs/plugins/list-extended.d.ts +2 -0
  139. package/dist/cjs/plugins/list-extended.js +235 -0
  140. package/dist/cjs/plugins/list-status.d.ts +6 -0
  141. package/dist/cjs/plugins/list-status.js +42 -0
  142. package/dist/cjs/plugins/literalminus.d.ts +8 -0
  143. package/dist/cjs/plugins/literalminus.js +32 -0
  144. package/dist/cjs/plugins/literalplus.d.ts +6 -0
  145. package/dist/cjs/plugins/literalplus.js +30 -0
  146. package/dist/cjs/plugins/logindisabled.d.ts +5 -0
  147. package/dist/cjs/plugins/logindisabled.js +54 -0
  148. package/dist/cjs/plugins/messagelimit.d.ts +19 -0
  149. package/dist/cjs/plugins/messagelimit.js +227 -0
  150. package/dist/cjs/plugins/metadata-server.d.ts +7 -0
  151. package/dist/cjs/plugins/metadata-server.js +24 -0
  152. package/dist/cjs/plugins/metadata.d.ts +11 -0
  153. package/dist/cjs/plugins/metadata.js +436 -0
  154. package/dist/cjs/plugins/move.d.ts +11 -0
  155. package/dist/cjs/plugins/move.js +96 -0
  156. package/dist/cjs/plugins/multiappend.d.ts +12 -0
  157. package/dist/cjs/plugins/multiappend.js +38 -0
  158. package/dist/cjs/plugins/multisearch.d.ts +6 -0
  159. package/dist/cjs/plugins/multisearch.js +263 -0
  160. package/dist/cjs/plugins/namespace.d.ts +5 -0
  161. package/dist/cjs/plugins/namespace.js +57 -0
  162. package/dist/cjs/plugins/notify.d.ts +2 -0
  163. package/dist/cjs/plugins/notify.js +628 -0
  164. package/dist/cjs/plugins/oauthbearer.d.ts +32 -0
  165. package/dist/cjs/plugins/oauthbearer.js +202 -0
  166. package/dist/cjs/plugins/objectid.d.ts +23 -0
  167. package/dist/cjs/plugins/objectid.js +221 -0
  168. package/dist/cjs/plugins/partial.d.ts +6 -0
  169. package/dist/cjs/plugins/partial.js +78 -0
  170. package/dist/cjs/plugins/preview.d.ts +31 -0
  171. package/dist/cjs/plugins/preview.js +378 -0
  172. package/dist/cjs/plugins/qresync.d.ts +11 -0
  173. package/dist/cjs/plugins/qresync.js +473 -0
  174. package/dist/cjs/plugins/quota.d.ts +18 -0
  175. package/dist/cjs/plugins/quota.js +254 -0
  176. package/dist/cjs/plugins/replace.d.ts +14 -0
  177. package/dist/cjs/plugins/replace.js +130 -0
  178. package/dist/cjs/plugins/sasl-ir.d.ts +5 -0
  179. package/dist/cjs/plugins/sasl-ir.js +24 -0
  180. package/dist/cjs/plugins/savedate.d.ts +17 -0
  181. package/dist/cjs/plugins/savedate.js +64 -0
  182. package/dist/cjs/plugins/savelimit.d.ts +10 -0
  183. package/dist/cjs/plugins/savelimit.js +30 -0
  184. package/dist/cjs/plugins/searchres.d.ts +6 -0
  185. package/dist/cjs/plugins/searchres.js +91 -0
  186. package/dist/cjs/plugins/sort-display.d.ts +11 -0
  187. package/dist/cjs/plugins/sort-display.js +37 -0
  188. package/dist/cjs/plugins/sort.d.ts +11 -0
  189. package/dist/cjs/plugins/sort.js +125 -0
  190. package/dist/cjs/plugins/special-use.d.ts +10 -0
  191. package/dist/cjs/plugins/special-use.js +95 -0
  192. package/dist/cjs/plugins/starttls.d.ts +5 -0
  193. package/dist/cjs/plugins/starttls.js +55 -0
  194. package/dist/cjs/plugins/status-size.d.ts +7 -0
  195. package/dist/cjs/plugins/status-size.js +30 -0
  196. package/dist/cjs/plugins/thread-orderedsubject.d.ts +11 -0
  197. package/dist/cjs/plugins/thread-orderedsubject.js +28 -0
  198. package/dist/cjs/plugins/thread-references.d.ts +11 -0
  199. package/dist/cjs/plugins/thread-references.js +28 -0
  200. package/dist/cjs/plugins/uidonly.d.ts +15 -0
  201. package/dist/cjs/plugins/uidonly.js +126 -0
  202. package/dist/cjs/plugins/uidplus.d.ts +15 -0
  203. package/dist/cjs/plugins/uidplus.js +119 -0
  204. package/dist/cjs/plugins/unauthenticate.d.ts +7 -0
  205. package/dist/cjs/plugins/unauthenticate.js +32 -0
  206. package/dist/cjs/plugins/unselect.d.ts +5 -0
  207. package/dist/cjs/plugins/unselect.js +36 -0
  208. package/dist/cjs/plugins/utf8-accept.d.ts +6 -0
  209. package/dist/cjs/plugins/utf8-accept.js +72 -0
  210. package/dist/cjs/plugins/x-gm-ext-1.d.ts +26 -0
  211. package/dist/cjs/plugins/x-gm-ext-1.js +422 -0
  212. package/dist/cjs/plugins/xoauth2.d.ts +2 -0
  213. package/dist/cjs/plugins/xoauth2.js +155 -0
  214. package/dist/cjs/plugins/xtoybird.d.ts +2 -0
  215. package/dist/cjs/plugins/xtoybird.js +236 -0
  216. package/dist/cjs/server.d.ts +876 -0
  217. package/dist/cjs/server.js +2597 -0
  218. package/dist/cjs/smtp-listener.d.ts +13 -0
  219. package/dist/cjs/smtp-listener.js +47 -0
  220. package/dist/cjs/sorting.d.ts +77 -0
  221. package/dist/cjs/sorting.js +290 -0
  222. package/dist/cjs/threading.d.ts +30 -0
  223. package/dist/cjs/threading.js +318 -0
  224. package/dist/cjs/types.d.ts +327 -0
  225. package/dist/cjs/types.js +3 -0
  226. package/dist/cjs/utf8-session.d.ts +17 -0
  227. package/dist/cjs/utf8-session.js +148 -0
  228. package/dist/cjs/vanished.d.ts +21 -0
  229. package/dist/cjs/vanished.js +55 -0
  230. package/dist/esm/addressparser.d.ts +21 -0
  231. package/{lib → dist/esm}/addressparser.js +102 -123
  232. package/dist/esm/arguments.d.ts +31 -0
  233. package/dist/esm/arguments.js +96 -0
  234. package/dist/esm/bodystructure.d.ts +25 -0
  235. package/{lib → dist/esm}/bodystructure.js +17 -33
  236. package/dist/esm/cert.d.ts +2 -0
  237. package/dist/esm/cert.js +4 -0
  238. package/dist/esm/command-states.d.ts +36 -0
  239. package/{lib → dist/esm}/command-states.js +4 -16
  240. package/dist/esm/commands/append.d.ts +88 -0
  241. package/{lib → dist/esm}/commands/append.js +47 -85
  242. package/dist/esm/commands/capability.d.ts +2 -0
  243. package/dist/esm/commands/capability.js +29 -0
  244. package/dist/esm/commands/check.d.ts +2 -0
  245. package/dist/esm/commands/check.js +13 -0
  246. package/dist/esm/commands/close.d.ts +2 -0
  247. package/dist/esm/commands/close.js +20 -0
  248. package/dist/esm/commands/copy.d.ts +30 -0
  249. package/{lib → dist/esm}/commands/copy.js +31 -55
  250. package/dist/esm/commands/create.d.ts +2 -0
  251. package/{lib → dist/esm}/commands/create.js +25 -39
  252. package/dist/esm/commands/delete.d.ts +2 -0
  253. package/{lib → dist/esm}/commands/delete.js +25 -41
  254. package/dist/esm/commands/examine.d.ts +2 -0
  255. package/dist/esm/commands/examine.js +4 -0
  256. package/dist/esm/commands/expunge.d.ts +2 -0
  257. package/dist/esm/commands/expunge.js +17 -0
  258. package/dist/esm/commands/fetch.d.ts +23 -0
  259. package/{lib → dist/esm}/commands/fetch.js +34 -75
  260. package/dist/esm/commands/handlers/fetch.d.ts +12 -0
  261. package/{lib → dist/esm}/commands/handlers/fetch.js +11 -42
  262. package/dist/esm/commands/handlers/flags.d.ts +17 -0
  263. package/{lib → dist/esm}/commands/handlers/flags.js +3 -11
  264. package/dist/esm/commands/handlers/search.d.ts +85 -0
  265. package/{lib → dist/esm}/commands/handlers/search.js +33 -71
  266. package/dist/esm/commands/handlers/status.d.ts +34 -0
  267. package/{lib → dist/esm}/commands/handlers/status.js +1 -9
  268. package/dist/esm/commands/handlers/store.d.ts +3 -0
  269. package/{lib → dist/esm}/commands/handlers/store.js +20 -44
  270. package/dist/esm/commands/index.d.ts +2 -0
  271. package/dist/esm/commands/index.js +55 -0
  272. package/dist/esm/commands/list.d.ts +2 -0
  273. package/dist/esm/commands/list.js +71 -0
  274. package/dist/esm/commands/login.d.ts +2 -0
  275. package/{lib → dist/esm}/commands/login.js +24 -44
  276. package/dist/esm/commands/logout.d.ts +2 -0
  277. package/dist/esm/commands/logout.js +25 -0
  278. package/dist/esm/commands/lsub.d.ts +2 -0
  279. package/dist/esm/commands/lsub.js +64 -0
  280. package/dist/esm/commands/noop.d.ts +2 -0
  281. package/dist/esm/commands/noop.js +13 -0
  282. package/dist/esm/commands/rename.d.ts +2 -0
  283. package/{lib → dist/esm}/commands/rename.js +32 -51
  284. package/dist/esm/commands/search.d.ts +10 -0
  285. package/dist/esm/commands/search.js +54 -0
  286. package/dist/esm/commands/select.d.ts +19 -0
  287. package/dist/esm/commands/select.js +214 -0
  288. package/dist/esm/commands/status.d.ts +2 -0
  289. package/{lib → dist/esm}/commands/status.js +18 -39
  290. package/dist/esm/commands/store.d.ts +10 -0
  291. package/{lib → dist/esm}/commands/store.js +39 -73
  292. package/dist/esm/commands/subscribe.d.ts +2 -0
  293. package/{lib → dist/esm}/commands/subscribe.js +22 -39
  294. package/dist/esm/commands/uid-copy.d.ts +2 -0
  295. package/dist/esm/commands/uid-copy.js +4 -0
  296. package/dist/esm/commands/uid-fetch.d.ts +3 -0
  297. package/dist/esm/commands/uid-fetch.js +3 -0
  298. package/dist/esm/commands/uid-search.d.ts +3 -0
  299. package/dist/esm/commands/uid-search.js +3 -0
  300. package/dist/esm/commands/uid-store.d.ts +3 -0
  301. package/dist/esm/commands/uid-store.js +3 -0
  302. package/dist/esm/commands/unsubscribe.d.ts +2 -0
  303. package/{lib → dist/esm}/commands/unsubscribe.js +22 -37
  304. package/dist/esm/dates.d.ts +64 -0
  305. package/{lib → dist/esm}/dates.js +6 -14
  306. package/dist/esm/deflate-layer.d.ts +83 -0
  307. package/{lib → dist/esm}/deflate-layer.js +9 -26
  308. package/dist/esm/encoded-words.d.ts +15 -0
  309. package/{lib → dist/esm}/encoded-words.js +9 -25
  310. package/dist/esm/envelope.d.ts +33 -0
  311. package/{lib → dist/esm}/envelope.js +8 -19
  312. package/dist/esm/esearch.d.ts +96 -0
  313. package/{lib → dist/esm}/esearch.js +12 -39
  314. package/dist/esm/framing.d.ts +66 -0
  315. package/dist/esm/framing.js +82 -0
  316. package/dist/esm/index.d.ts +9 -0
  317. package/dist/esm/index.js +6 -0
  318. package/dist/esm/list-extensions.d.ts +50 -0
  319. package/{lib → dist/esm}/list-extensions.js +1 -4
  320. package/dist/esm/load-plugins.d.ts +21 -0
  321. package/{lib → dist/esm}/load-plugins.js +9 -43
  322. package/dist/esm/mailbox-name.d.ts +26 -0
  323. package/{lib → dist/esm}/mailbox-name.js +5 -26
  324. package/dist/esm/mimeparser.d.ts +137 -0
  325. package/{lib → dist/esm}/mimeparser.js +74 -152
  326. package/dist/esm/mock-client.d.ts +39 -0
  327. package/{lib → dist/esm}/mock-client.js +26 -40
  328. package/dist/esm/numbers.d.ts +28 -0
  329. package/{lib → dist/esm}/numbers.js +1 -8
  330. package/dist/esm/package.json +3 -0
  331. package/dist/esm/plugins/acl.d.ts +2 -0
  332. package/{lib → dist/esm}/plugins/acl.js +146 -240
  333. package/dist/esm/plugins/appendlimit.d.ts +16 -0
  334. package/{lib → dist/esm}/plugins/appendlimit.js +5 -16
  335. package/dist/esm/plugins/auth-plain.d.ts +2 -0
  336. package/{lib → dist/esm}/plugins/auth-plain.js +11 -31
  337. package/dist/esm/plugins/binary.d.ts +30 -0
  338. package/{lib → dist/esm}/plugins/binary.js +17 -43
  339. package/dist/esm/plugins/catenate.d.ts +14 -0
  340. package/{lib → dist/esm}/plugins/catenate.js +19 -40
  341. package/dist/esm/plugins/compress.d.ts +7 -0
  342. package/dist/esm/plugins/compress.js +61 -0
  343. package/dist/esm/plugins/condstore.d.ts +5 -0
  344. package/{lib → dist/esm}/plugins/condstore.js +66 -152
  345. package/dist/esm/plugins/context-search.d.ts +8 -0
  346. package/{lib → dist/esm}/plugins/context-search.js +53 -88
  347. package/dist/esm/plugins/context-sort.d.ts +6 -0
  348. package/{lib → dist/esm}/plugins/context-sort.js +4 -9
  349. package/dist/esm/plugins/create-special-use.d.ts +7 -0
  350. package/{lib → dist/esm}/plugins/create-special-use.js +33 -55
  351. package/dist/esm/plugins/enable.d.ts +23 -0
  352. package/dist/esm/plugins/enable.js +111 -0
  353. package/dist/esm/plugins/esearch.d.ts +6 -0
  354. package/{lib → dist/esm}/plugins/esearch.js +13 -42
  355. package/dist/esm/plugins/esort.d.ts +6 -0
  356. package/{lib → dist/esm}/plugins/esort.js +7 -25
  357. package/dist/esm/plugins/id.d.ts +5 -0
  358. package/dist/esm/plugins/id.js +108 -0
  359. package/dist/esm/plugins/idle.d.ts +5 -0
  360. package/dist/esm/plugins/idle.js +70 -0
  361. package/dist/esm/plugins/imap4rev2.d.ts +6 -0
  362. package/{lib → dist/esm}/plugins/imap4rev2.js +24 -60
  363. package/dist/esm/plugins/index.d.ts +2 -0
  364. package/dist/esm/plugins/index.js +113 -0
  365. package/dist/esm/plugins/list-extended.d.ts +2 -0
  366. package/{lib → dist/esm}/plugins/list-extended.js +28 -66
  367. package/dist/esm/plugins/list-status.d.ts +6 -0
  368. package/{lib → dist/esm}/plugins/list-status.js +5 -12
  369. package/dist/esm/plugins/literalminus.d.ts +8 -0
  370. package/{lib → dist/esm}/plugins/literalminus.js +2 -5
  371. package/dist/esm/plugins/literalplus.d.ts +6 -0
  372. package/{lib → dist/esm}/plugins/literalplus.js +2 -5
  373. package/dist/esm/plugins/logindisabled.d.ts +5 -0
  374. package/dist/esm/plugins/logindisabled.js +39 -0
  375. package/dist/esm/plugins/messagelimit.d.ts +19 -0
  376. package/{lib → dist/esm}/plugins/messagelimit.js +18 -41
  377. package/dist/esm/plugins/metadata-server.d.ts +7 -0
  378. package/{lib → dist/esm}/plugins/metadata-server.js +3 -7
  379. package/dist/esm/plugins/metadata.d.ts +11 -0
  380. package/dist/esm/plugins/metadata.js +421 -0
  381. package/dist/esm/plugins/move.d.ts +11 -0
  382. package/{lib → dist/esm}/plugins/move.js +37 -66
  383. package/dist/esm/plugins/multiappend.d.ts +12 -0
  384. package/{lib → dist/esm}/plugins/multiappend.js +2 -5
  385. package/dist/esm/plugins/multisearch.d.ts +6 -0
  386. package/{lib → dist/esm}/plugins/multisearch.js +24 -48
  387. package/dist/esm/plugins/namespace.d.ts +5 -0
  388. package/dist/esm/plugins/namespace.js +42 -0
  389. package/dist/esm/plugins/notify.d.ts +2 -0
  390. package/{lib → dist/esm}/plugins/notify.js +90 -134
  391. package/dist/esm/plugins/oauthbearer.d.ts +32 -0
  392. package/{lib → dist/esm}/plugins/oauthbearer.js +18 -48
  393. package/dist/esm/plugins/objectid.d.ts +23 -0
  394. package/{lib → dist/esm}/plugins/objectid.js +21 -58
  395. package/dist/esm/plugins/partial.d.ts +6 -0
  396. package/{lib → dist/esm}/plugins/partial.js +7 -15
  397. package/dist/esm/plugins/preview.d.ts +31 -0
  398. package/{lib → dist/esm}/plugins/preview.js +28 -66
  399. package/dist/esm/plugins/qresync.d.ts +11 -0
  400. package/{lib → dist/esm}/plugins/qresync.js +64 -134
  401. package/dist/esm/plugins/quota.d.ts +18 -0
  402. package/{lib → dist/esm}/plugins/quota.js +89 -135
  403. package/dist/esm/plugins/replace.d.ts +14 -0
  404. package/{lib → dist/esm}/plugins/replace.js +21 -51
  405. package/dist/esm/plugins/sasl-ir.d.ts +5 -0
  406. package/{lib → dist/esm}/plugins/sasl-ir.js +3 -6
  407. package/dist/esm/plugins/savedate.d.ts +17 -0
  408. package/{lib → dist/esm}/plugins/savedate.js +5 -15
  409. package/dist/esm/plugins/savelimit.d.ts +10 -0
  410. package/{lib → dist/esm}/plugins/savelimit.js +3 -6
  411. package/dist/esm/plugins/searchres.d.ts +6 -0
  412. package/{lib → dist/esm}/plugins/searchres.js +13 -22
  413. package/dist/esm/plugins/sort-display.d.ts +11 -0
  414. package/{lib → dist/esm}/plugins/sort-display.js +6 -10
  415. package/dist/esm/plugins/sort.d.ts +11 -0
  416. package/{lib → dist/esm}/plugins/sort.js +24 -46
  417. package/dist/esm/plugins/special-use.d.ts +10 -0
  418. package/{lib → dist/esm}/plugins/special-use.js +9 -24
  419. package/dist/esm/plugins/starttls.d.ts +5 -0
  420. package/{lib → dist/esm}/plugins/starttls.js +20 -37
  421. package/dist/esm/plugins/status-size.d.ts +7 -0
  422. package/{lib → dist/esm}/plugins/status-size.js +4 -11
  423. package/dist/esm/plugins/thread-orderedsubject.d.ts +11 -0
  424. package/{lib → dist/esm}/plugins/thread-orderedsubject.js +3 -6
  425. package/dist/esm/plugins/thread-references.d.ts +11 -0
  426. package/{lib → dist/esm}/plugins/thread-references.js +3 -6
  427. package/dist/esm/plugins/uidonly.d.ts +15 -0
  428. package/{lib → dist/esm}/plugins/uidonly.js +11 -35
  429. package/dist/esm/plugins/uidplus.d.ts +15 -0
  430. package/{lib → dist/esm}/plugins/uidplus.js +25 -45
  431. package/dist/esm/plugins/unauthenticate.d.ts +7 -0
  432. package/dist/esm/plugins/unauthenticate.js +17 -0
  433. package/dist/esm/plugins/unselect.d.ts +5 -0
  434. package/dist/esm/plugins/unselect.js +21 -0
  435. package/dist/esm/plugins/utf8-accept.d.ts +6 -0
  436. package/{lib → dist/esm}/plugins/utf8-accept.js +10 -21
  437. package/dist/esm/plugins/x-gm-ext-1.d.ts +26 -0
  438. package/{lib → dist/esm}/plugins/x-gm-ext-1.js +48 -100
  439. package/dist/esm/plugins/xoauth2.d.ts +2 -0
  440. package/dist/esm/plugins/xoauth2.js +140 -0
  441. package/dist/esm/plugins/xtoybird.d.ts +2 -0
  442. package/dist/esm/plugins/xtoybird.js +218 -0
  443. package/dist/esm/server.d.ts +876 -0
  444. package/dist/esm/server.js +2543 -0
  445. package/dist/esm/smtp-listener.d.ts +13 -0
  446. package/{lib → dist/esm}/smtp-listener.js +5 -12
  447. package/dist/esm/sorting.d.ts +77 -0
  448. package/{lib → dist/esm}/sorting.js +40 -53
  449. package/dist/esm/threading.d.ts +30 -0
  450. package/{lib → dist/esm}/threading.js +29 -70
  451. package/dist/esm/types.d.ts +327 -0
  452. package/dist/esm/types.js +2 -0
  453. package/dist/esm/utf8-session.d.ts +17 -0
  454. package/{lib → dist/esm}/utf8-session.js +15 -25
  455. package/dist/esm/vanished.d.ts +21 -0
  456. package/{lib → dist/esm}/vanished.js +16 -21
  457. package/dist/plugin-help.json +298 -0
  458. package/package.json +54 -11
  459. package/lib/commands/capability.js +0 -47
  460. package/lib/commands/check.js +0 -21
  461. package/lib/commands/close.js +0 -30
  462. package/lib/commands/examine.js +0 -7
  463. package/lib/commands/expunge.js +0 -27
  464. package/lib/commands/list.js +0 -100
  465. package/lib/commands/logout.js +0 -41
  466. package/lib/commands/lsub.js +0 -87
  467. package/lib/commands/noop.js +0 -21
  468. package/lib/commands/search.js +0 -76
  469. package/lib/commands/select.js +0 -289
  470. package/lib/commands/uid copy.js +0 -7
  471. package/lib/commands/uid fetch.js +0 -5
  472. package/lib/commands/uid search.js +0 -5
  473. package/lib/commands/uid store.js +0 -5
  474. package/lib/plugins/compress.js +0 -76
  475. package/lib/plugins/enable.js +0 -155
  476. package/lib/plugins/id.js +0 -138
  477. package/lib/plugins/idle.js +0 -105
  478. package/lib/plugins/logindisabled.js +0 -50
  479. package/lib/plugins/metadata.js +0 -475
  480. package/lib/plugins/namespace.js +0 -67
  481. package/lib/plugins/unauthenticate.js +0 -28
  482. package/lib/plugins/unselect.js +0 -36
  483. package/lib/plugins/xoauth2.js +0 -188
  484. package/lib/plugins/xtoybird.js +0 -282
  485. package/lib/server.js +0 -2880
@@ -0,0 +1,2543 @@
1
+ import { Stream } from 'node:stream';
2
+ import net from 'node:net';
3
+ import tls from 'node:tls';
4
+ import imapHandler from 'imap-handler';
5
+ import formalSyntax from 'imap-handler/lib/formal';
6
+ import loadPlugins from './load-plugins.js';
7
+ import { commands as builtinCommands } from './commands/index.js';
8
+ import { getCommandOptions, commandOptions } from './command-states.js';
9
+ import validateMailboxName from './mailbox-name.js';
10
+ import { MONTHS, monthIndex, isRealDate } from './dates.js';
11
+ import fetchHandlers from './commands/handlers/fetch.js';
12
+ import { hasSequenceSetKey } from './commands/handlers/search.js';
13
+ import { isSequenceSet } from './numbers.js';
14
+ import { restoreNilAtoms } from './arguments.js';
15
+ import { refuseMissingTarget } from './commands/append.js';
16
+ import * as bundledCert from './cert.js';
17
+ // longest command line (not counting literals) accepted from a client
18
+ const MAX_LINE_LENGTH = 1024 * 1024;
19
+ // largest literal accepted after login, override with the maxLiteralSize option
20
+ const MAX_LITERAL_SIZE = 64 * 1024 * 1024;
21
+ // largest literal accepted before login, enough for any user name or password
22
+ const MAX_PREAUTH_LITERAL_SIZE = 64 * 1024;
23
+ const LITERAL_TOO_LARGE = 'Literal too large';
24
+ // status responses, their text must follow the RFC 3501 section 9 resp-text rules
25
+ const STATUS_RESPONSES = new Set(['OK', 'NO', 'BAD', 'BYE', 'PREAUTH']);
26
+ // RFC 3501 section 9: tag = 1*<any ASTRING-CHAR except "+">
27
+ const TAG_REGEX = new RegExp('^[' + formalSyntax.tag().replace(/[\\\]^-]/g, '\\$&') + ']+$');
28
+ // RFC 3501 section 9: atom = 1*ATOM-CHAR
29
+ const ATOM_CHARS = '[' + formalSyntax['ATOM-CHAR']().replace(/[\\\]^-]/g, '\\$&') + ']+';
30
+ const ATOM_REGEX = new RegExp('^' + ATOM_CHARS + '$');
31
+ // RFC 3501 section 9: a command name, and the second word of UID and AUTHENTICATE, is an atom
32
+ const COMMAND_REGEX = new RegExp('^' + ATOM_CHARS + '( ' + ATOM_CHARS + ')?$');
33
+ // RFC spelling of the mailbox attributes the server checks or computes (RFC 3501 section 7.2.2, RFC 3348
34
+ // section 3, RFC 5258 section 3), keyed by lowercase name
35
+ const MAILBOX_ATTRIBUTES = new Map(['\\Noinferiors', '\\Noselect', '\\Marked', '\\Unmarked', '\\HasChildren', '\\HasNoChildren', '\\NonExistent'].map(flag => [flag.toLowerCase(), flag]));
36
+ /**
37
+ * Returns the tag to use when answering a raw command line that could not be parsed. A line
38
+ * without a valid tag is answered untagged, a client could not parse the invalid tag anyway.
39
+ *
40
+ * @param {String} line Raw command line
41
+ * @return {String} tag or "*"
42
+ */
43
+ function getResponseTag(line) {
44
+ // only SP separates the tag (RFC 3501 section 9: command = tag SP ...)
45
+ const space = line.indexOf(' ');
46
+ const tag = space >= 0 ? line.substr(0, space) : line;
47
+ return tag && TAG_REGEX.test(tag) ? tag : '*';
48
+ }
49
+ /**
50
+ * Text for a command that is not valid in the current connection state
51
+ *
52
+ * @param {String} command Upper case command name
53
+ * @param {String} state Connection state
54
+ * @return {String} Error text
55
+ */
56
+ function stateError(command, state) {
57
+ return command + ' is not allowed in the ' + state + ' state';
58
+ }
59
+ /**
60
+ * Creates an error for a failed mailbox operation, with a RFC 5530 response code
61
+ *
62
+ * @param {String} message Error message
63
+ * @param {String} code Response code, e.g. "ALREADYEXISTS"
64
+ * @return {Error} Error object
65
+ */
66
+ function mailboxError(message, code) {
67
+ const err = new Error(message);
68
+ err.code = code;
69
+ return err;
70
+ }
71
+ /**
72
+ * Creates a new IMAP server, call `listen()` on it to start accepting connections
73
+ *
74
+ * @param options Server options, the mailbox tree comes from `options.storage`
75
+ * @return Server instance
76
+ */
77
+ export default function server(options) {
78
+ return new IMAPServer(options);
79
+ }
80
+ class IMAPServer extends Stream {
81
+ constructor(options) {
82
+ super();
83
+ // shallow copy, so that the caller's options object is never modified
84
+ this.options = Object.assign({}, options);
85
+ if (this.options.secureConnection) {
86
+ this.server = tls.createServer(this.getCredentials(), this.createClient.bind(this));
87
+ }
88
+ else {
89
+ this.server = net.createServer(this.createClient.bind(this));
90
+ }
91
+ // every connection listens to the notify event
92
+ this.setMaxListeners(0);
93
+ this.connections = new Set();
94
+ this.connectionHandlers = [];
95
+ // run when a connection returns to the Not Authenticated state (UNAUTHENTICATE), each one
96
+ // clears the per-session state its plugin keeps on the connection
97
+ this.resetHandlers = [];
98
+ this.outputHandlers = [];
99
+ this.messageHandlers = [];
100
+ this.fetchHandlers = {};
101
+ this.fetchFilters = [];
102
+ this.searchHandlers = {};
103
+ this.storeHandlers = {};
104
+ this.storeFilters = [];
105
+ // `filter(connection, notification)` functions, a notification only reaches connections they all accept
106
+ this.notifyFilters = [];
107
+ // run on every mailbox in processMailbox, like messageHandlers for messages
108
+ this.mailboxHandlers = [];
109
+ // consulted before messages are added to a mailbox by APPEND, COPY or MOVE, see IMAPConnection#checkAppend
110
+ this.appendChecks = [];
111
+ // carry properties over when a message is copied to another mailbox (COPY, MOVE, RENAME INBOX), see copyMessage
112
+ this.copyHandlers = [];
113
+ // append-data extensions such as CATENATE (RFC 4466 section 2.7), and checks that can refuse
114
+ // a synchronizing literal before it is read, see IMAPConnection#checkLiteral
115
+ this.appendDataHandlers = Object.create(null);
116
+ // APPEND and REPLACE to a mailbox that does not exist are refused before the message is sent
117
+ this.literalFilters = [refuseMissingTarget];
118
+ // can refuse IMAP URLs that read a mailbox (CATENATE), `(connection, mailbox, url)` returns `{ text }` to refuse
119
+ this.urlAccessChecks = [];
120
+ // can leave mailboxes out of a search of several mailboxes (ESEARCH of MULTISEARCH), `(connection, mailbox, named)`
121
+ // returns false for a mailbox that is skipped, `named` is true if the client gave its name
122
+ this.searchAccessChecks = [];
123
+ // run before a command handler, `(connection, parsed)` returns `{ command, code, text }` to refuse the command
124
+ // (e.g. commands with message sequence numbers after ENABLE UIDONLY), see IMAPConnection#processQueue
125
+ this.commandChecks = [];
126
+ // `(connection, parsed, range)` functions that can cut the messages that FETCH, STORE, COPY, MOVE and UID EXPUNGE
127
+ // (and the UID variants) operate on, e.g. MESSAGELIMIT. See IMAPConnection#limitRange
128
+ this.rangeLimits = [];
129
+ // `(connection, messages, query)` functions that can narrow down the messages a SEARCH (or SORT, THREAD) looks
130
+ // at by returning a shorter list, e.g. MESSAGELIMIT. See commands/handlers/search.ts
131
+ this.searchLimits = [];
132
+ // `check(connection)` functions, SELECT and EXAMINE send `* OK [CLOSED]` when they close the selected mailbox
133
+ // if any of them is true (CONDSTORE, RFC 7162 section 3.2.11, IMAP4rev2, RFC 9051 section 6.3.2)
134
+ this.closedChecks = [];
135
+ // set by MULTIAPPEND (RFC 3502), otherwise APPEND takes a single message
136
+ this.multiAppend = false;
137
+ // the built-in handlers, setCommandHandler replaces them. Without a prototype, a command name like
138
+ // "TOSTRING" never finds an inherited function
139
+ this.commandHandlers = Object.assign(Object.create(null), builtinCommands);
140
+ // options of commands that plugins add, core commands are listed in command-states.ts
141
+ this.commandOptions = Object.create(null);
142
+ this.capabilities = {};
143
+ this.allowedStatus = ['MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN'];
144
+ // values of STATUS items that plugins add, consulted before the built-in items in commands/handlers/status.ts
145
+ this.statusHandlers = {};
146
+ // non-synchronizing literals {n+} are accepted when literalPlus is set (LITERAL+ and LITERAL-),
147
+ // up to nonSyncLiteralLimit octets (4096 for LITERAL-, RFC 7888 section 5)
148
+ this.literalPlus = false;
149
+ this.nonSyncLiteralLimit = Infinity;
150
+ // extra options for the imap-handler command parser, e.g. literal8 for BINARY
151
+ this.parserOptions = {
152
+ // items that take a [section] and <partial>, the imap-handler default
153
+ allowSection: ['BODY', 'BODY.PEEK']
154
+ };
155
+ this.referenceNamespace = false;
156
+ // the session whose command is running, see IMAPServer#notify
157
+ this.activeConnection = null;
158
+ // users and storage are deep copied, so that runtime changes never leak into
159
+ // the caller's objects or into other servers built from the same fixture.
160
+ // Without a prototype, user names like "__proto__" or "toString" are plain keys
161
+ this.users = Object.assign(Object.create(null), this.options.users
162
+ ? structuredClone(this.options.users)
163
+ : {
164
+ testuser: {
165
+ password: 'testpass',
166
+ xoauth2: {
167
+ accessToken: 'testtoken',
168
+ sessionTimeout: 3600 * 1000
169
+ }
170
+ }
171
+ });
172
+ loadPlugins(this, this.options.plugins);
173
+ this.systemFlags = [].concat(this.options.systemFlags || ['\\Answered', '\\Flagged', '\\Draft', '\\Deleted', '\\Seen']);
174
+ // indexFolders() below turns the storage option into namespaces and mailboxes in place
175
+ this.storage = (this.options.storage
176
+ ? structuredClone(this.options.storage)
177
+ : {
178
+ INBOX: {},
179
+ '': {}
180
+ });
181
+ this.uidvalidityCounter = 0; // highest UIDVALIDITY in use, new mailboxes get a higher one
182
+ // subscribed mailbox names (RFC 3501 section 6.3.6). Names, not mailboxes: a subscription outlives
183
+ // DELETE and stays with the old name on RENAME (RFC 9051 section 6.3.6), see trackSubscription
184
+ this.subscriptions = new Set();
185
+ this.folderCache = Object.create(null);
186
+ this.indexFolders(true);
187
+ }
188
+ listen(...args) {
189
+ this.server.listen.apply(this.server, args);
190
+ }
191
+ close(callback) {
192
+ this.server.close(callback);
193
+ // close() only completes once all connections are gone
194
+ this.connections.forEach((connection) => {
195
+ if (connection.socket) {
196
+ connection.socket.destroy();
197
+ }
198
+ });
199
+ }
200
+ /**
201
+ * Returns TLS key and certificate. Without credentials in the options the
202
+ * bundled self-signed certificate for localhost is used.
203
+ *
204
+ * @return {Object} TLS options
205
+ */
206
+ getCredentials() {
207
+ if (!this.options.credentials) {
208
+ this.options.credentials = {
209
+ key: bundledCert.key,
210
+ cert: bundledCert.cert
211
+ };
212
+ }
213
+ return this.options.credentials;
214
+ }
215
+ address() {
216
+ return this.server.address();
217
+ }
218
+ createClient(socket) {
219
+ const connection = new IMAPConnection(this, socket);
220
+ this.connectionHandlers.forEach(handler => {
221
+ handler(connection);
222
+ });
223
+ }
224
+ registerCapability(keyword, handler) {
225
+ this.capabilities[keyword] =
226
+ handler ||
227
+ function () {
228
+ return true;
229
+ };
230
+ }
231
+ /**
232
+ * Sets the handler of a command
233
+ *
234
+ * @param {String} command Command name, e.g. "UID MOVE"
235
+ * @param {Function} handler Command handler `(connection, parsed, data, callback)`
236
+ * @param {Object|Array} [options] `{ states, noArguments, mailboxArguments, astringArguments, searchCriteria, sequenceSet,
237
+ * noExpunge, literal8, noPipelining }`: the connection states the command is valid in (any state if not set), if it takes
238
+ * no arguments, the positions of its mailbox name arguments and of its other astring arguments, the position where its search criteria start, the position of its argument
239
+ * with message sequence numbers, if EXPUNGE responses
240
+ * are not allowed while it runs, if it accepts literal8 arguments (true, or the name of the capability that
241
+ * allows them), and if it is refused when the client sent more input after it. A list is read as the states.
242
+ * Without options, a command keeps its earlier settings
243
+ */
244
+ setCommandHandler(command, handler, options) {
245
+ command = (command || '').toString().toUpperCase();
246
+ this.commandHandlers[command] = handler;
247
+ if (options) {
248
+ this.commandOptions[command] = commandOptions(options);
249
+ }
250
+ }
251
+ /**
252
+ * Returns the options of a command, see setCommandHandler
253
+ *
254
+ * @param {String} command Command name
255
+ * @return {Object} the command options, see setCommandHandler, states is false if any state is fine
256
+ */
257
+ getCommandOptions(command) {
258
+ command = (command || '').toString().toUpperCase();
259
+ return this.commandOptions[command] || getCommandOptions(command) || commandOptions();
260
+ }
261
+ /**
262
+ * Returns the connection states a command may be used in. Public API for custom plugins (see README), the server
263
+ * itself reads getCommandOptions
264
+ *
265
+ * @param {String} command Command name
266
+ * @return {Array|Boolean} List of states, or false if any state is fine
267
+ */
268
+ getCommandStates(command) {
269
+ return this.getCommandOptions(command).states;
270
+ }
271
+ /**
272
+ * Returns a user account
273
+ *
274
+ * @param {String} username User name
275
+ * @return {Object|false} User data or false if there is no such user
276
+ */
277
+ getUser(username) {
278
+ return (typeof username === 'string' && this.users[username]) || false;
279
+ }
280
+ /**
281
+ * Returns a mailbox object from folderCache
282
+ *
283
+ * @param {String} path Pathname for the mailbox
284
+ * @return {Object} mailbox object or undefined
285
+ */
286
+ getMailbox(path) {
287
+ if (path.toUpperCase() === 'INBOX') {
288
+ return this.folderCache.INBOX;
289
+ }
290
+ return this.folderCache[path];
291
+ }
292
+ /**
293
+ * Schedules a notifying message
294
+ *
295
+ * @param {Object} command An object of untagged response message
296
+ * @param {Object|String} mailbox Mailbox the message is related to
297
+ * @param {Object} ignoreConnection if set the selected connection ignores this notification
298
+ * @param {Function} [filter] if set, only connections for which `filter(connection)` is true get the notification
299
+ */
300
+ notify(command, mailbox, ignoreConnection, filter) {
301
+ command.notification = true;
302
+ const event = {
303
+ command: command,
304
+ mailbox: mailbox,
305
+ ignoreConnection: ignoreConnection,
306
+ filter: filter,
307
+ // the session whose command caused the change, null for changes from outside (e.g. SMTP)
308
+ origin: this.activeConnection
309
+ };
310
+ this.emit('notify', event);
311
+ }
312
+ /**
313
+ * Tells plugins that a mailbox was created, deleted, renamed, subscribed or unsubscribed, with a
314
+ * `mailbox` event: `{ type, path, oldPath, mailbox, origin }`. `type` is "create", "delete", "rename",
315
+ * "subscribe" or "unsubscribe", `origin` is the session that made the change
316
+ *
317
+ * @param {String} type Kind of change
318
+ * @param {String} path Storage name of the mailbox
319
+ * @param {Object} [details] `{ oldPath, mailbox }`: the earlier name of a renamed mailbox, the mailbox
320
+ * object that a DELETE removed
321
+ */
322
+ mailboxChanged(type, path, details) {
323
+ const event = Object.assign({ type, path, oldPath: null, mailbox: null }, details, { origin: this.activeConnection });
324
+ this.emit('mailbox', event);
325
+ }
326
+ /**
327
+ * Retrieves the handler of an IMAP command
328
+ *
329
+ * @param {String} command Command name
330
+ * @return {Function} handler for the specified command
331
+ */
332
+ getCommandHandler(command) {
333
+ return this.commandHandlers[(command || '').toString().toUpperCase()] || false;
334
+ }
335
+ /**
336
+ * Returns some useful information about a mailbox that can be used with STATUS, SELECT and EXAMINE
337
+ *
338
+ * @param {Object|String} mailbox Mailbox object or path
339
+ */
340
+ getStatus(path) {
341
+ const mailbox = typeof path === 'string' ? this.getMailbox(path) : path;
342
+ if (!mailbox) {
343
+ return false;
344
+ }
345
+ const flags = {};
346
+ let seen = 0;
347
+ let unseen = 0;
348
+ // flags stay defined in the mailbox once a message had them, see rememberFlags
349
+ const permanentFlags = [].concat(mailbox.permanentFlags || []);
350
+ (mailbox.knownFlags || []).forEach(flag => this.ensureFlag(permanentFlags, flag));
351
+ let recent = 0;
352
+ // \Recent sets of the sessions that have this mailbox selected
353
+ const recentSets = [];
354
+ this.connections.forEach(connection => {
355
+ if (connection.selectedMailbox === mailbox && connection.recent) {
356
+ recentSets.push(connection.recent);
357
+ }
358
+ });
359
+ mailbox.messages.forEach(message => {
360
+ if (message.flags.indexOf('\\Seen') < 0) {
361
+ unseen++;
362
+ }
363
+ else {
364
+ seen++;
365
+ }
366
+ if (message.recent || recentSets.some(set => set.has(message))) {
367
+ recent++;
368
+ }
369
+ message.flags.forEach(flag => {
370
+ if (!flags[flag]) {
371
+ flags[flag] = 1;
372
+ }
373
+ else {
374
+ flags[flag]++;
375
+ }
376
+ if (permanentFlags.indexOf(flag) < 0) {
377
+ permanentFlags.push(flag);
378
+ }
379
+ });
380
+ });
381
+ return {
382
+ flags: flags,
383
+ seen: seen,
384
+ unseen: unseen,
385
+ recent: recent,
386
+ permanentFlags: permanentFlags
387
+ };
388
+ }
389
+ /**
390
+ * Validates a date value. Useful for validating APPEND dates
391
+ *
392
+ * @param {String} date Date value to be validated
393
+ * @return {Boolean} Returns true if the date string is in IMAP date-time format
394
+ */
395
+ validateInternalDate(date) {
396
+ if (!date || typeof date !== 'string') {
397
+ return false;
398
+ }
399
+ // date-time from RFC 3501 section 9, month names are case-insensitive like all ABNF strings
400
+ const match = date.match(/^( \d|\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{4}) (\d{2}):(\d{2}):(\d{2}) [-+](\d{2})(\d{2})$/i);
401
+ if (!match) {
402
+ return false;
403
+ }
404
+ // the values must also make a real date and time
405
+ return (isRealDate(match[1], monthIndex(match[2]), match[3]) &&
406
+ Number(match[4]) < 24 &&
407
+ Number(match[5]) < 60 &&
408
+ Number(match[6]) < 61 &&
409
+ Number(match[8]) < 60);
410
+ }
411
+ /**
412
+ * Converts a date object to a valid date-time string format
413
+ *
414
+ * @param {Object} date Date object to be converted
415
+ * @return {String} Returns a valid date-time formatted string
416
+ */
417
+ formatInternalDate(date) {
418
+ const day = date.getDate();
419
+ const month = MONTHS[date.getMonth()];
420
+ const year = date.getFullYear();
421
+ const hour = date.getHours();
422
+ const minute = date.getMinutes();
423
+ const second = date.getSeconds();
424
+ const tz = date.getTimezoneOffset();
425
+ const tzHours = Math.floor(Math.abs(tz) / 60);
426
+ const tzMins = Math.abs(tz) % 60;
427
+ return ((day < 10 ? '0' : '') +
428
+ day +
429
+ '-' +
430
+ month +
431
+ '-' +
432
+ year +
433
+ ' ' +
434
+ (hour < 10 ? '0' : '') +
435
+ hour +
436
+ ':' +
437
+ (minute < 10 ? '0' : '') +
438
+ minute +
439
+ ':' +
440
+ (second < 10 ? '0' : '') +
441
+ second +
442
+ ' ' +
443
+ (tz > 0 ? '-' : '+') +
444
+ (tzHours < 10 ? '0' : '') +
445
+ tzHours +
446
+ (tzMins < 10 ? '0' : '') +
447
+ tzMins);
448
+ }
449
+ /**
450
+ * Creates a mailbox with specified path
451
+ *
452
+ * @param {String} path Pathname for the mailbox
453
+ * @param {Object} [defaultMailbox] use this object as the mailbox to add instead of empty'
454
+ * @return {Object} the created mailbox
455
+ */
456
+ createMailbox(path, defaultMailbox) {
457
+ if (!path) {
458
+ throw mailboxError('Invalid mailbox name', 'CANNOT');
459
+ }
460
+ // Ensure case insensitive INBOX
461
+ if (path.toUpperCase() === 'INBOX') {
462
+ throw mailboxError('INBOX can not be modified', 'ALREADYEXISTS');
463
+ }
464
+ const { namespace, storage } = this.getPersonalNamespace(path);
465
+ path = this.stripSeparator(path, storage.separator);
466
+ if (this.folderCache[path] && this.folderCache[path].flags.indexOf('\\Noselect') < 0) {
467
+ throw mailboxError('Mailbox already exists', 'ALREADYEXISTS');
468
+ }
469
+ const folderPath = path.substr(namespace.length).split(storage.separator);
470
+ if (folderPath.some(name => !name)) {
471
+ // an empty hierarchy level ("foo//bar", "/foo", "foo//"). RFC 5530 section 3 has this very case as the
472
+ // example of CANNOT, Dovecot refuses it too
473
+ throw mailboxError('Mailbox names can not have empty hierarchy levels', 'CANNOT');
474
+ }
475
+ let parent = storage;
476
+ let curPath = namespace;
477
+ if (curPath) {
478
+ curPath = curPath.substr(0, curPath.length - storage.separator.length);
479
+ }
480
+ folderPath.forEach(folderName => {
481
+ curPath += (curPath.length ? storage.separator : '') + folderName;
482
+ let folder = this.getMailbox(curPath) || false;
483
+ if (folder && folder.flags && folder.flags.indexOf('\\Noinferiors') >= 0) {
484
+ throw mailboxError('Can not create subfolders for ' + folder.path, 'CANNOT');
485
+ }
486
+ // a \Noselect placeholder that is created again is replaced with a new mailbox that only keeps
487
+ // the children, nothing else of a deleted mailbox may come back (RFC 3501 section 6.3.3)
488
+ const isTarget = curPath === path;
489
+ const useDefault = isTarget && defaultMailbox;
490
+ if (!folder || useDefault || (isTarget && folder.flags.indexOf('\\Noselect') >= 0)) {
491
+ const children = folder && folder.folders;
492
+ // a recreated mailbox must never reuse an earlier UIDVALIDITY value
493
+ const created = useDefault ? defaultMailbox : { uidvalidity: ++this.uidvalidityCounter };
494
+ if (children) {
495
+ created.folders = Object.assign({}, children, created.folders);
496
+ }
497
+ // a new mailbox is subscribed if its name is, a subscription is not part of the mailbox
498
+ this.trackSubscription(created);
499
+ this.processMailbox(curPath, created, namespace);
500
+ parent.folders = parent.folders || {};
501
+ parent.folders[folderName] = created;
502
+ this.folderCache[curPath] = created;
503
+ folder = created;
504
+ }
505
+ if (parent !== storage) {
506
+ // Remove \HasNoChildren and add \\HasChildren from parent. A \Noselect parent stays \Noselect,
507
+ // it already is the hierarchy level the new mailbox needs
508
+ this.setChildrenFlags(parent, true);
509
+ }
510
+ else if (folder.namespace === this.referenceNamespace && this.inboxHoldsNamespace()) {
511
+ this.setChildrenFlags(this.storage.INBOX, true);
512
+ }
513
+ parent = folder;
514
+ });
515
+ return this.folderCache[path];
516
+ }
517
+ /**
518
+ * Deletes a mailbox with specified path
519
+ *
520
+ * @param {String} path Pathname for the mailbox
521
+ * @param {boolean} keepContents If true do not delete messages
522
+ */
523
+ deleteMailbox(path, keepContents) {
524
+ // Ensure case insensitive INBOX
525
+ if (path.toUpperCase() === 'INBOX') {
526
+ throw mailboxError('INBOX can not be modified', 'CANNOT');
527
+ }
528
+ const { namespace, storage } = this.getPersonalNamespace(path);
529
+ const mailbox = this.folderCache[this.stripSeparator(path, storage.separator)];
530
+ if (!mailbox) {
531
+ throw mailboxError('Mailbox does not exist', 'NONEXISTENT');
532
+ }
533
+ if (mailbox.flags.indexOf('\\Noselect') >= 0 && Object.keys(mailbox.folders || {}).length) {
534
+ // RFC 9051 section 6.3.5: deleting a \Noselect name that has inferior names is an error, the RFC 5530
535
+ // section 3 HASCHILDREN response code tells the client to delete the children first
536
+ throw mailboxError('Mailbox has children, delete them first', 'HASCHILDREN');
537
+ }
538
+ const levels = mailbox.path.split(storage.separator);
539
+ const folderName = levels.pop();
540
+ const parentKey = levels.join(storage.separator);
541
+ const parent = (parentKey !== 'INBOX' && this.folderCache[parentKey]) || storage;
542
+ if (mailbox.folders && Object.keys(mailbox.folders).length && !keepContents) {
543
+ // Sessions that have the mailbox selected keep the old object, a new SELECT finds the
544
+ // placeholder. The placeholder only keeps the children. Plugin data (MAILBOXID, special-use, metadata, ACL,
545
+ // HIGHESTMODSEQ ...) belongs to the deleted mailbox and must not survive (RFC 3501 section 6.3.4)
546
+ const folder = {
547
+ flags: ['\\Noselect'],
548
+ folders: mailbox.folders
549
+ };
550
+ this.trackSubscription(folder);
551
+ this.processMailbox(mailbox.path, folder, mailbox.namespace);
552
+ parent.folders[folderName] = folder;
553
+ this.folderCache[mailbox.path] = folder;
554
+ return;
555
+ }
556
+ delete this.folderCache[mailbox.path];
557
+ delete parent.folders[folderName];
558
+ if (parent !== storage) {
559
+ if (parent.flags.indexOf('\\Noselect') >= 0 && !Object.keys(parent.folders || {}).length) {
560
+ this.deleteMailbox(parent.path);
561
+ }
562
+ else {
563
+ this.setChildrenFlags(parent, Object.keys(parent.folders || {}).length > 0);
564
+ }
565
+ }
566
+ else if (namespace === this.referenceNamespace && this.inboxHoldsNamespace()) {
567
+ this.setChildrenFlags(this.storage.INBOX, Object.keys(storage.folders || {}).length > 0);
568
+ }
569
+ }
570
+ /**
571
+ * Finds the personal namespace that a new or existing mailbox name belongs to, for CREATE and DELETE
572
+ *
573
+ * @param {String} path Mailbox path
574
+ * @return {Object} `{ namespace, storage }`, the namespace key and its storage object
575
+ * @throws {Error} CANNOT for a namespace prefix or a name in no namespace, NOPERM outside personal namespaces
576
+ */
577
+ getPersonalNamespace(path) {
578
+ let namespace = '';
579
+ Object.keys(this.storage).forEach(key => {
580
+ if (key === 'INBOX') {
581
+ return;
582
+ }
583
+ const prefix = key.length ? key.substr(0, key.length - this.storage[key].separator.length) : key;
584
+ if (key.length && (path === prefix || path.substr(0, key.length) === key)) {
585
+ if (path === prefix) {
586
+ throw mailboxError('Used mailbox name is a namespace value', 'CANNOT');
587
+ }
588
+ namespace = key;
589
+ }
590
+ else if (!namespace && !key && this.storage[key].type === 'personal') {
591
+ namespace = key;
592
+ }
593
+ });
594
+ const storage = this.storage[namespace];
595
+ if (!storage) {
596
+ throw mailboxError('Unknown namespace', 'CANNOT');
597
+ }
598
+ if (storage.type !== 'personal') {
599
+ throw mailboxError('Permission denied', 'NOPERM');
600
+ }
601
+ return { namespace, storage };
602
+ }
603
+ /**
604
+ * Removes a trailing hierarchy separator from a mailbox name, "foo/" names the mailbox "foo"
605
+ *
606
+ * @param {String} path Mailbox path
607
+ * @param {String} separator Hierarchy separator
608
+ * @return {String} path without the separator at the end
609
+ */
610
+ stripSeparator(path, separator) {
611
+ return path.substr(-separator.length) === separator ? path.substr(0, path.length - separator.length) : path;
612
+ }
613
+ /**
614
+ * Checks if the personal namespace is below INBOX (e.g. "INBOX."), then the mailboxes of that namespace are
615
+ * the children of INBOX
616
+ *
617
+ * @return {Boolean} true if the reference namespace is INBOX followed by the separator
618
+ */
619
+ inboxHoldsNamespace() {
620
+ const reference = this.referenceNamespace;
621
+ if (reference === false) {
622
+ return false;
623
+ }
624
+ const namespace = this.storage[reference];
625
+ return !!namespace && reference.substr(0, reference.length - namespace.separator.length).toUpperCase() === 'INBOX';
626
+ }
627
+ /**
628
+ * Rebuilds folderCache and the path, namespace and flags of every mailbox from storage.
629
+ * INBOX has its own namespace
630
+ *
631
+ * @param {Boolean} [processMessages] If true, messages are prepared as well. Only needed for
632
+ * messages from the initial storage, as message handlers must not run twice for a message
633
+ */
634
+ indexFolders(processMessages) {
635
+ const folders = Object.create(null);
636
+ const walkTree = (path, separator, branch, namespace) => {
637
+ Object.keys(branch).forEach(key => {
638
+ const curBranch = branch[key];
639
+ const curPath = (path ? path + (path.substr(-1) !== separator ? separator : '') : '') + key;
640
+ this.processMailbox(curPath, curBranch, namespace);
641
+ folders[curPath] = curBranch;
642
+ if (processMessages) {
643
+ this.processMessages(curBranch);
644
+ }
645
+ if (curBranch.folders && Object.keys(curBranch.folders).length) {
646
+ walkTree(curPath, separator, curBranch.folders, namespace);
647
+ }
648
+ });
649
+ };
650
+ // Ensure INBOX namespace always exists, processMailbox() below makes it a mailbox
651
+ if (!this.storage.INBOX) {
652
+ this.storage.INBOX = {};
653
+ }
654
+ Object.keys(this.storage).forEach(key => {
655
+ if (key !== 'INBOX') {
656
+ this.storage[key].folders = this.storage[key].folders || {};
657
+ // "INBOX." uses "." as the separator, but "#news" does not use "s"
658
+ this.storage[key].separator = this.storage[key].separator || (/[^a-z0-9]$/i.test(key) ? key.substr(-1) : '/');
659
+ this.storage[key].type = this.storage[key].type || 'personal';
660
+ if (this.storage[key].type === 'personal' && this.referenceNamespace === false) {
661
+ this.referenceNamespace = key;
662
+ }
663
+ walkTree(key, this.storage[key].separator, this.storage[key].folders, key);
664
+ }
665
+ });
666
+ if (!this.referenceNamespace) {
667
+ // the keys are set below
668
+ this.storage[''] = this.storage[''] || {};
669
+ this.storage[''].folders = this.storage[''].folders || {};
670
+ this.storage[''].separator = this.storage[''].separator || '/';
671
+ this.storage[''].type = 'personal';
672
+ this.referenceNamespace = '';
673
+ }
674
+ // referenceNamespace is set by now
675
+ if (!this.storage.INBOX.separator) {
676
+ this.storage.INBOX.separator = this.storage[this.referenceNamespace].separator;
677
+ }
678
+ // INBOX is its own namespace, but its subfolders belong to the personal namespace
679
+ folders.INBOX = this.storage.INBOX;
680
+ this.processMailbox('INBOX', this.storage.INBOX, 'INBOX');
681
+ if (processMessages) {
682
+ this.processMessages(this.storage.INBOX);
683
+ }
684
+ if (this.storage.INBOX.folders && Object.keys(this.storage.INBOX.folders).length) {
685
+ walkTree('INBOX', this.storage.INBOX.separator, this.storage.INBOX.folders, this.referenceNamespace);
686
+ }
687
+ if (this.inboxHoldsNamespace()) {
688
+ this.setChildrenFlags(this.storage.INBOX, Object.keys(this.storage[this.referenceNamespace].folders || {}).length > 0);
689
+ }
690
+ this.folderCache = folders;
691
+ }
692
+ /**
693
+ * Ensures uid, flags and internaldate for every message of a mailbox and
694
+ * keeps the message list ordered by UID
695
+ *
696
+ * @param {Object} mailbox Mailbox object
697
+ */
698
+ processMessages(mailbox) {
699
+ const seen = new Set();
700
+ // messages from storage, before they are processed
701
+ const messages = mailbox.messages;
702
+ messages.forEach((entry, i) => {
703
+ // If the input was a raw message, convert it to an object
704
+ const message = typeof entry === 'string'
705
+ ? (messages[i] = {
706
+ raw: entry
707
+ })
708
+ : entry;
709
+ this.processMessage(message, mailbox);
710
+ if (seen.has(message.uid)) {
711
+ throw new Error('Duplicate UID ' + message.uid + ' in mailbox ' + mailbox.path);
712
+ }
713
+ seen.add(message.uid);
714
+ });
715
+ mailbox.messages.sort((a, b) => a.uid - b.uid);
716
+ }
717
+ /**
718
+ * Sets the path, namespace, flags and counters of a mailbox from storage or a new one, and runs the
719
+ * mailbox handlers of plugins on it
720
+ *
721
+ * @param path Mailbox path
722
+ * @param mailbox Mailbox object, completed in place
723
+ * @param namespace Namespace key of the mailbox
724
+ */
725
+ processMailbox(path, mailbox, namespace) {
726
+ mailbox.path = path;
727
+ mailbox.namespace = namespace;
728
+ mailbox.uid = mailbox.uid || 1;
729
+ mailbox.uidvalidity = mailbox.uidvalidity || 1;
730
+ this.uidvalidityCounter = Math.max(this.uidvalidityCounter, mailbox.uidvalidity);
731
+ // mailbox attributes are case-insensitive (RFC 3501 section 9, note 1), storage may spell them in any case,
732
+ // the checks and responses use the RFC spelling
733
+ mailbox.flags = [].concat(mailbox.flags || []).map(flag => MAILBOX_ATTRIBUTES.get(String(flag).toLowerCase()) || flag);
734
+ mailbox.allowPermanentFlags = 'allowPermanentFlags' in mailbox ? mailbox.allowPermanentFlags : true;
735
+ mailbox.permanentFlags = [].concat(mailbox.permanentFlags || this.systemFlags);
736
+ // a mailbox from storage is subscribed unless it says otherwise
737
+ this.trackSubscription(mailbox, true);
738
+ // ensure message array
739
+ const messages = [].concat(mailbox.messages || []);
740
+ mailbox.messages = messages;
741
+ // ensure highest uidnext
742
+ mailbox.uidnext = Math.max(...[mailbox.uidnext || 1].concat(messages.map(message => {
743
+ // a raw message (string) has no UID yet
744
+ return ((typeof message === 'string' ? 0 : message.uid) || 0) + 1;
745
+ })));
746
+ const processed = mailbox;
747
+ this.setChildrenFlags(processed, Object.keys(processed.folders || {}).length > 0);
748
+ // Allow plugins to process mailboxes
749
+ this.mailboxHandlers.forEach(handler => {
750
+ handler(this, processed);
751
+ });
752
+ }
753
+ /**
754
+ * Makes `mailbox.subscribed` read and change the subscription of the mailbox name in
755
+ * `server.subscriptions`, so that the subscription stays with the name when the mailbox is deleted
756
+ * or renamed (RFC 3501 section 6.3.6, RFC 9051 section 6.3.6). A `subscribed` value the mailbox
757
+ * already has (from storage) is moved over to the subscription list.
758
+ *
759
+ * @param {Object} mailbox Mailbox object, its `path` is read whenever the subscription is used
760
+ * @param {Boolean} [defaultValue] Subscription of a mailbox without a `subscribed` value. If not
761
+ * set, the subscription list is left as it is
762
+ */
763
+ trackSubscription(mailbox, defaultValue) {
764
+ const descriptor = Object.getOwnPropertyDescriptor(mailbox, 'subscribed');
765
+ if (descriptor && descriptor.get) {
766
+ return;
767
+ }
768
+ const value = descriptor ? !!descriptor.value : defaultValue;
769
+ Object.defineProperty(mailbox, 'subscribed', {
770
+ enumerable: true,
771
+ configurable: true,
772
+ get: () => this.subscriptions.has(mailbox.path),
773
+ set: (subscribed) => {
774
+ if (subscribed) {
775
+ this.subscriptions.add(mailbox.path);
776
+ }
777
+ else {
778
+ this.subscriptions.delete(mailbox.path);
779
+ }
780
+ }
781
+ });
782
+ if (typeof value === 'boolean') {
783
+ mailbox.subscribed = value;
784
+ }
785
+ }
786
+ /**
787
+ * Sets the stored children attribute of a mailbox (RFC 3348 section 3). A \Noinferiors mailbox gets
788
+ * neither, \Noinferiors already implies \HasNoChildren (RFC 5258 section 3.4)
789
+ *
790
+ * @param {Object} mailbox Mailbox object
791
+ * @param {Boolean} hasChildren true if the mailbox has child mailboxes
792
+ */
793
+ setChildrenFlags(mailbox, hasChildren) {
794
+ this.removeFlag(mailbox.flags, '\\HasChildren');
795
+ this.removeFlag(mailbox.flags, '\\HasNoChildren');
796
+ if (mailbox.flags.indexOf('\\Noinferiors') < 0) {
797
+ mailbox.flags.push(hasChildren ? '\\HasChildren' : '\\HasNoChildren');
798
+ }
799
+ }
800
+ /**
801
+ * Ensures that a list of flags includes selected flag
802
+ *
803
+ * @param {Array} flags An array of flags to check
804
+ * @param {String} flag If the flag is missing, add it
805
+ */
806
+ ensureFlag(flags, flag) {
807
+ if (flags.indexOf(flag) < 0) {
808
+ flags.push(flag);
809
+ }
810
+ }
811
+ /**
812
+ * Removes a flag from a list of flags
813
+ *
814
+ * @param {Array} flags An array of flags to check
815
+ * @param {String} flag If the flag is in the list, remove it
816
+ */
817
+ removeFlag(flags, flag) {
818
+ let i;
819
+ if (flags.indexOf(flag) >= 0) {
820
+ for (i = flags.length - 1; i >= 0; i--) {
821
+ if (flags[i] === flag) {
822
+ flags.splice(i, 1);
823
+ }
824
+ }
825
+ }
826
+ }
827
+ /**
828
+ * Remembers the flags of a message as flags of the mailbox. A keyword stays in the FLAGS and
829
+ * PERMANENTFLAGS of the mailbox after the last message with it is expunged or loses it, like a
830
+ * keyword a client defined (RFC 3501 section 2.3.2, FLAGS lists the flags applicable for the
831
+ * mailbox, section 7.2.6)
832
+ *
833
+ * @param {Object} mailbox Mailbox object
834
+ * @param {Array} flags Flags of a message
835
+ */
836
+ rememberFlags(mailbox, flags) {
837
+ const knownFlags = (mailbox.knownFlags = mailbox.knownFlags || []);
838
+ flags.forEach(flag => this.ensureFlag(knownFlags, flag));
839
+ }
840
+ normalizeDateTime(value) {
841
+ if (value instanceof Date) {
842
+ return this.formatInternalDate(value);
843
+ }
844
+ if (typeof value === 'string') {
845
+ // month names are accepted in any case but always sent as "Jan", "Feb", ...
846
+ return value.replace(/-([a-z]{3})-/i, (m, month) => '-' + (MONTHS[monthIndex(month)] || month) + '-');
847
+ }
848
+ return value;
849
+ }
850
+ /**
851
+ * Sets the UID, flags, internal date and source of a message from storage or a new one, and runs the
852
+ * message handlers of plugins on it
853
+ *
854
+ * @param message Message object, completed in place
855
+ * @param mailbox Mailbox of the message
856
+ */
857
+ processMessage(message, mailbox) {
858
+ message.internaldate = this.normalizeDateTime(message.internaldate || new Date());
859
+ message.flags = [].concat(message.flags || []);
860
+ if (message.flags.indexOf('\\Recent') >= 0) {
861
+ // \Recent is not a stored flag, it belongs to the first session that selects the mailbox
862
+ this.removeFlag(message.flags, '\\Recent');
863
+ message.recent = true;
864
+ }
865
+ this.rememberFlags(mailbox, message.flags);
866
+ message.uid = message.uid || mailbox.uidnext++;
867
+ if (message.uid >= mailbox.uidnext) {
868
+ mailbox.uidnext = message.uid + 1;
869
+ }
870
+ // message source is kept as a binary string (one character per octet)
871
+ if (message.raw instanceof Uint8Array) {
872
+ message.raw = Buffer.from(message.raw).toString('binary');
873
+ }
874
+ else if (typeof message.raw !== 'string') {
875
+ message.raw = message.raw ? String(message.raw) : '';
876
+ }
877
+ if (/[\u0100-\uffff]/.test(message.raw)) {
878
+ // characters outside Latin-1 can only come from a unicode string, so encode it as UTF-8
879
+ message.raw = Buffer.from(message.raw, 'utf-8').toString('binary');
880
+ }
881
+ // Allow plugins to process messages
882
+ const processed = message;
883
+ this.messageHandlers.forEach(handler => {
884
+ handler(this, processed, mailbox);
885
+ });
886
+ }
887
+ /**
888
+ * Appends a message to a mailbox
889
+ *
890
+ * @param {Object|String} mailbox Mailbox to append to
891
+ * @param {Array} flags Flags for the message
892
+ * @param {String|Date} internaldate Receive date-time for the message
893
+ * @param {String} raw Message source
894
+ * @param {Object} [ignoreConnection] To not advertise new message to selected connection
895
+ * @param {Object} [properties] More properties of the new message, set before message handlers run
896
+ * @return An object of the form { mailbox, message }
897
+ */
898
+ appendMessage(path, flags, internaldate, raw, ignoreConnection, properties) {
899
+ const mailbox = typeof path === 'string' ? this.getMailbox(path) : path;
900
+ // processMessage() below sets the UID
901
+ const message = Object.assign({}, properties, {
902
+ flags: flags,
903
+ internaldate: internaldate,
904
+ raw: raw,
905
+ recent: true
906
+ });
907
+ mailbox.messages.push(message);
908
+ this.processMessage(message, mailbox);
909
+ // a session that has the mailbox selected read-write sees the new message as \Recent
910
+ for (const connection of this.connections) {
911
+ if (connection.selectedMailbox === mailbox && !connection.readOnly && connection.recent) {
912
+ connection.recent.add(message);
913
+ delete message.recent;
914
+ break;
915
+ }
916
+ }
917
+ this.notify({
918
+ tag: '*',
919
+ attributes: [
920
+ mailbox.messages.length,
921
+ {
922
+ type: 'ATOM',
923
+ value: 'EXISTS'
924
+ }
925
+ ],
926
+ // the new message, for plugins that report more about it (e.g. NOTIFY)
927
+ message: message
928
+ }, mailbox, ignoreConnection);
929
+ return { mailbox: mailbox, message: message };
930
+ }
931
+ /**
932
+ * Copies a message to a mailbox (COPY, MOVE, RENAME INBOX). The copy is a new message that
933
+ * keeps the flags, internal date and content of the source. `copyHandlers` can carry over more
934
+ * properties of the source, they run before the message handlers see the copy
935
+ *
936
+ * @param {Object} mailbox Target mailbox
937
+ * @param {Object} source Message to copy
938
+ * @return An object of the form { mailbox, message }
939
+ */
940
+ copyMessage(mailbox, source) {
941
+ const properties = {};
942
+ this.copyHandlers.forEach(handler => {
943
+ handler(this, source, properties, mailbox);
944
+ });
945
+ return this.appendMessage(mailbox, [].concat(source.flags || []), source.internaldate, source.raw, false, properties);
946
+ }
947
+ /**
948
+ * Returns the namespace a mailbox path belongs to by its prefix, INBOX not included
949
+ *
950
+ * @param {String} path Mailbox path, it does not have to exist
951
+ * @return {String|Boolean} the longest matching namespace key, or false
952
+ */
953
+ getNamespace(path) {
954
+ let namespace = false;
955
+ Object.keys(this.storage).forEach(key => {
956
+ if (key !== 'INBOX' && path.substr(0, key.length) === key && (namespace === false || key.length > namespace.length)) {
957
+ namespace = key;
958
+ }
959
+ });
960
+ return namespace;
961
+ }
962
+ /**
963
+ * Checks if messages can be added to a mailbox (APPEND, COPY, MOVE). TRYCREATE tells the client that CREATE would
964
+ * help (RFC 3501 sections 6.3.11 and 6.4.7), also for a \\Noselect name, which CREATE turns into a mailbox (RFC 9051
965
+ * sections 6.3.12 and 6.4.7: unless it is certain that the target can not be created)
966
+ *
967
+ * @param {String} path Storage name of the target mailbox
968
+ * @return {Object|Boolean} `{ command, code, text }` of the refusal, or false if the mailbox can take messages
969
+ */
970
+ targetRefusal(path) {
971
+ const mailbox = this.getMailbox(path);
972
+ if (!mailbox) {
973
+ return { command: 'NO', code: 'TRYCREATE', text: 'Target mailbox does not exist' };
974
+ }
975
+ if (mailbox.flags.indexOf('\\Noselect') >= 0) {
976
+ return { command: 'NO', code: 'TRYCREATE', text: 'Target mailbox is not selectable' };
977
+ }
978
+ return false;
979
+ }
980
+ /**
981
+ * Returns the namespace of a mailbox name: "INBOX" for INBOX, the namespace of the mailbox if it
982
+ * exists (the children of INBOX belong to the personal namespace), otherwise the one of its prefix
983
+ *
984
+ * @param {String|Object} path Mailbox path, it does not have to exist, or a mailbox object
985
+ * @return {String|Boolean} namespace key, or false if the name is in no namespace
986
+ */
987
+ getMailboxNamespace(path) {
988
+ if (path && typeof path === 'object') {
989
+ return path.namespace;
990
+ }
991
+ if (path.toUpperCase() === 'INBOX') {
992
+ return 'INBOX';
993
+ }
994
+ const mailbox = this.getMailbox(path);
995
+ return mailbox ? mailbox.namespace : this.getNamespace(path);
996
+ }
997
+ /**
998
+ * Checks if a mailbox name is in a personal namespace, INBOX included
999
+ *
1000
+ * @param {String|Object} path Mailbox path, it does not have to exist, or a mailbox object
1001
+ * @return {Boolean} true for a personal mailbox
1002
+ */
1003
+ isPersonal(path) {
1004
+ const key = this.getMailboxNamespace(path);
1005
+ return key === 'INBOX' || (key !== false && !!this.storage[key] && this.storage[key].type === 'personal');
1006
+ }
1007
+ /**
1008
+ * Returns the hierarchy separator of a mailbox name, from its namespace
1009
+ *
1010
+ * @param {String|Object} path Mailbox path, it does not have to exist, or a mailbox object
1011
+ * @return {String} separator
1012
+ */
1013
+ getSeparator(path) {
1014
+ const key = this.getMailboxNamespace(path);
1015
+ const namespace = key === false ? undefined : this.storage[key];
1016
+ return (namespace && namespace.separator) || this.storage.INBOX.separator || '/';
1017
+ }
1018
+ /**
1019
+ * Returns the name one hierarchy level up from a mailbox name. A trailing separator is ignored, the
1020
+ * prefix of a namespace (e.g. "#shared" of "#shared/") is not a mailbox name
1021
+ *
1022
+ * @param {String} path Mailbox path, it does not have to exist
1023
+ * @return {String|Boolean} parent name, "INBOX" for the children of INBOX, or false at the top level
1024
+ */
1025
+ getParentPath(path) {
1026
+ const separator = this.getSeparator(path);
1027
+ path = this.stripSeparator(path, separator);
1028
+ const index = path.lastIndexOf(separator);
1029
+ if (index <= 0) {
1030
+ return false;
1031
+ }
1032
+ const parent = path.substr(0, index);
1033
+ if (parent.toUpperCase() === 'INBOX') {
1034
+ return 'INBOX';
1035
+ }
1036
+ return this.storage[parent + separator] ? false : parent;
1037
+ }
1038
+ getDescendants(path, folders) {
1039
+ const source = folders || this.folderCache;
1040
+ const prefix = path + this.getSeparator(path);
1041
+ return Object.keys(source)
1042
+ .filter(key => key.substr(0, prefix.length) === prefix)
1043
+ .map(key => source[key]);
1044
+ }
1045
+ /**
1046
+ * Checks if any mailbox below a mailbox name passes a test, stops at the first one that does
1047
+ *
1048
+ * @param {String} path Mailbox path
1049
+ * @param {Function} predicate `(mailbox)` returns true for a match
1050
+ * @return {Boolean} true if a mailbox below the name matches
1051
+ */
1052
+ hasDescendant(path, predicate) {
1053
+ const prefix = path + this.getSeparator(path);
1054
+ for (const key of Object.keys(this.folderCache)) {
1055
+ if (key.substr(0, prefix.length) === prefix && predicate(this.folderCache[key])) {
1056
+ return true;
1057
+ }
1058
+ }
1059
+ return false;
1060
+ }
1061
+ /**
1062
+ * Returns the mailbox attributes of a LIST response with computed children attributes (RFC 3348, RFC 5258
1063
+ * section 4), for extended and unsolicited LIST responses. The stored \HasChildren and \HasNoChildren are
1064
+ * replaced, a name that does not exist is \NonExistent (RFC 5258 section 3, it implies \Noselect)
1065
+ *
1066
+ * @param {Object} [mailbox] Mailbox object, can be left out for a name that does not exist
1067
+ * @param {Object} options `{ exists, subscribed, hasChildren, extra }`: false `exists` lists the name as
1068
+ * \NonExistent, `subscribed` adds \Subscribed, `extra` lists more attributes (e.g. \NoAccess)
1069
+ * @return {Array} attribute names
1070
+ */
1071
+ listAttributes(mailbox, options) {
1072
+ const exists = options.exists !== false;
1073
+ const flags = ((mailbox && mailbox.flags) || []).filter(flag => flag !== '\\HasChildren' && flag !== '\\HasNoChildren' && (exists || flag !== '\\Noselect'));
1074
+ if (!exists) {
1075
+ flags.push('\\NonExistent');
1076
+ }
1077
+ if (options.subscribed) {
1078
+ flags.push('\\Subscribed');
1079
+ }
1080
+ flags.push(...(options.extra || []));
1081
+ // \Noinferiors implies \HasNoChildren (RFC 5258 section 3.4 and section 4 example 3)
1082
+ if (flags.indexOf('\\Noinferiors') < 0) {
1083
+ flags.push(options.hasChildren ? '\\HasChildren' : '\\HasNoChildren');
1084
+ }
1085
+ return flags;
1086
+ }
1087
+ matchFolders(referenceName, match, exportName, folders) {
1088
+ let includeINBOX = false;
1089
+ const source = folders || this.folderCache;
1090
+ const toName = exportName || ((name) => name);
1091
+ let reference = referenceName || '';
1092
+ if (reference === '' && this.referenceNamespace !== false) {
1093
+ reference = toName(this.referenceNamespace);
1094
+ includeINBOX = true;
1095
+ }
1096
+ // the reference does not have to be a namespace, use the namespace it belongs to
1097
+ let nsKey = false;
1098
+ let nsName = '';
1099
+ for (const key of Object.keys(this.storage)) {
1100
+ const name = toName(key);
1101
+ if (key !== 'INBOX' && reference.substr(0, name.length) === name && (nsKey === false || name.length > nsName.length)) {
1102
+ nsKey = key;
1103
+ nsName = name;
1104
+ }
1105
+ }
1106
+ if (nsKey === false) {
1107
+ return [];
1108
+ }
1109
+ const namespace = this.storage[nsKey];
1110
+ const lookup = reference + match;
1111
+ const result = [];
1112
+ const pattern = '^' +
1113
+ lookup
1114
+ // escape regex symbols
1115
+ .replace(/([\\^$+?!.():=[\]{}|,-])/g, '\\$1')
1116
+ .replace(/[*]/g, '.*')
1117
+ .replace(/[%]/g, '[^' + namespace.separator.replace(/([\\^$+*?!.():=[\]{}|,-])/g, '\\$1') + ']*') +
1118
+ '$';
1119
+ const query = new RegExp(pattern, '');
1120
+ // INBOX is case-insensitive
1121
+ if (includeINBOX && source.INBOX && ((reference ? reference + namespace.separator : '') + 'INBOX').match(new RegExp(pattern, 'i'))) {
1122
+ result.push(source.INBOX);
1123
+ }
1124
+ Object.keys(source).forEach(path => {
1125
+ const folder = source[path];
1126
+ if (folder.namespace !== nsKey) {
1127
+ return;
1128
+ }
1129
+ const name = toName(path);
1130
+ if (name.match(query) && (folder.flags.indexOf('\\NonExistent') < 0 || name === match)) {
1131
+ result.push(folder);
1132
+ }
1133
+ });
1134
+ return result;
1135
+ }
1136
+ /**
1137
+ * Returns the subscribed names with their superior hierarchy levels, for LSUB and LIST (SUBSCRIBED).
1138
+ * Names that are not mailboxes get a stand-in object with \Noselect, which LIST-EXTENDED reports as
1139
+ * \NonExistent (RFC 5258 section 3), and `subscribed` false for a level that is only listed because
1140
+ * of a subscribed name below it
1141
+ *
1142
+ * @return {Object} path to mailbox object or stand-in, usable as the `folders` of matchFolders
1143
+ */
1144
+ getSubscriptionTree() {
1145
+ const tree = Object.create(null);
1146
+ const add = (path, subscribed) => {
1147
+ tree[path] = tree[path] || this.getMailbox(path) || { path, namespace: this.getNamespace(path), flags: ['\\Noselect'], subscribed };
1148
+ };
1149
+ const names = [...this.subscriptions].filter(path => path === 'INBOX' || this.getNamespace(path) !== false);
1150
+ names.forEach(path => add(path, true));
1151
+ names.forEach(path => {
1152
+ // superior levels of the name within its namespace
1153
+ for (let parent = this.getParentPath(path); parent; parent = this.getParentPath(parent)) {
1154
+ add(parent, false);
1155
+ }
1156
+ });
1157
+ return tree;
1158
+ }
1159
+ /**
1160
+ * Retrieves an array of messages that fit in the specified range criteria
1161
+ *
1162
+ * @param {Object|String} mailbox Mailbox to look for the messages
1163
+ * @param {String} range Message range (eg. "*:4,5,7:9")
1164
+ * @param {Boolean} isUid If true, use UID values, not sequence indexes for comparison
1165
+ * @return {Array} An array of messages in the form of [[seqIndex, message]]
1166
+ */
1167
+ getMessageRange(source, sequence, isUid) {
1168
+ const range = (sequence || '').toString();
1169
+ const mailbox = typeof source === 'string' ? this.getMailbox(source) : source;
1170
+ // sequence-set from RFC 3501 and RFC 9051 section 9, numbers are nz-number values (32-bit), UIDs too
1171
+ if (!isSequenceSet(range)) {
1172
+ const err = new Error('Invalid sequence set');
1173
+ err.imapResponse = 'BAD';
1174
+ throw err;
1175
+ }
1176
+ const result = [];
1177
+ const rangeParts = range.split(',');
1178
+ const messages = Array.isArray(mailbox) ? mailbox : mailbox.messages;
1179
+ let uid;
1180
+ const totalMessages = messages.length;
1181
+ let maxUid = 0;
1182
+ const inRange = function (nr, ranges, total) {
1183
+ for (let i = 0, len = ranges.length; i < len; i++) {
1184
+ const to = ranges[i].split(':');
1185
+ const first = to.shift();
1186
+ const from = Number(first === '*' ? total : first) || 1;
1187
+ const last = to.pop() || from;
1188
+ const end = Number((last === '*' && total) || last) || from;
1189
+ if (nr >= Math.min(from, end) && nr <= Math.max(from, end)) {
1190
+ return true;
1191
+ }
1192
+ }
1193
+ return false;
1194
+ };
1195
+ messages.forEach(message => {
1196
+ if (message.uid > maxUid) {
1197
+ maxUid = message.uid;
1198
+ }
1199
+ });
1200
+ for (let i = 0, len = messages.length; i < len; i++) {
1201
+ uid = messages[i].uid || 1;
1202
+ if (inRange(isUid ? uid : i + 1, rangeParts, isUid ? maxUid : totalMessages)) {
1203
+ result.push([i + 1, messages[i]]);
1204
+ }
1205
+ }
1206
+ return result;
1207
+ }
1208
+ }
1209
+ class IMAPConnection {
1210
+ constructor(server, socket) {
1211
+ this.server = server;
1212
+ this.socket = socket;
1213
+ this.options = this.server.options;
1214
+ this.state = 'Not Authenticated';
1215
+ this.secureConnection = !!this.options.secureConnection;
1216
+ this._remainder = '';
1217
+ this._command = '';
1218
+ this._literalRemaining = 0;
1219
+ this.inputHandler = false;
1220
+ // a layer between the socket and the IMAP protocol, such as COMPRESS=DEFLATE (RFC 4978). It has
1221
+ // the methods write(buffer), receive(chunk), end(callback) and destroy(), and passes data on with
1222
+ // connection.writeRaw() and connection.onData(), so it always sits above TLS (RFC 4978 section 3)
1223
+ this.transport = null;
1224
+ // per connection options for the imap-handler parser and compiler, plugins can change these. The
1225
+ // parser options go on top of server.parserOptions
1226
+ this.parserOptions = {};
1227
+ this.compilerOptions = {};
1228
+ // message/global parts encapsulate a message like message/rfc822 parts in BODYSTRUCTURE and in section
1229
+ // numbers. IMAP4rev2 sets it (RFC 9051 sections 6.4.5.1 and 7.5.2), IMAP4rev1 describes them as basic parts
1230
+ this.messageGlobal = false;
1231
+ this._commandQueue = [];
1232
+ this._processing = false;
1233
+ if (this.options.debug) {
1234
+ this.socket.pipe(process.stdout);
1235
+ }
1236
+ this.socket.on('data', this.receive.bind(this));
1237
+ this.socket.on('close', this.onClose.bind(this));
1238
+ this.socket.on('error', this.onError.bind(this));
1239
+ this.directNotifications = false;
1240
+ this._notificationCallback = this.onNotify.bind(this);
1241
+ this.notificationQueue = [];
1242
+ this.server.on('notify', this._notificationCallback);
1243
+ this.server.connections.add(this);
1244
+ this.write('* OK ImapKit ready for rumble\r\n');
1245
+ }
1246
+ /**
1247
+ * Writes protocol output to the client, through the transport layer if there is one
1248
+ *
1249
+ * @param {Buffer|String} data Data to send, a string is sent as a binary string
1250
+ */
1251
+ write(data) {
1252
+ const buffer = typeof data === 'string' ? Buffer.from(data, 'binary') : data;
1253
+ if (this.transport) {
1254
+ this.transport.write(buffer);
1255
+ }
1256
+ else {
1257
+ this.writeRaw(buffer);
1258
+ }
1259
+ }
1260
+ /**
1261
+ * Writes data to the socket, below the transport layer
1262
+ *
1263
+ * @param {Buffer} data Data to send
1264
+ */
1265
+ writeRaw(data) {
1266
+ if (this.socket && !this.socket.destroyed) {
1267
+ this.socket.write(data);
1268
+ }
1269
+ }
1270
+ /**
1271
+ * Handles data from the socket, through the transport layer if there is one
1272
+ *
1273
+ * @param {Buffer} chunk Received data
1274
+ */
1275
+ receive(chunk) {
1276
+ if (this.transport) {
1277
+ this.transport.receive(chunk);
1278
+ }
1279
+ else {
1280
+ this.onData(chunk);
1281
+ }
1282
+ }
1283
+ /**
1284
+ * Closes the connection once everything sent so far, including data a transport layer still
1285
+ * holds, is written out
1286
+ */
1287
+ end() {
1288
+ const socket = this.socket;
1289
+ if (!socket) {
1290
+ return;
1291
+ }
1292
+ this._closing = true;
1293
+ if (this.transport) {
1294
+ this.transport.end(() => socket.end());
1295
+ }
1296
+ else {
1297
+ socket.end();
1298
+ }
1299
+ }
1300
+ /**
1301
+ * Sends an untagged BYE, drops unprocessed input and closes the connection (RFC 3501 section 7.1.5)
1302
+ *
1303
+ * @param {String} text Human readable explanation
1304
+ * @param {String} [description] Description for output handlers
1305
+ */
1306
+ bye(text, description) {
1307
+ this.sendStatus({ tag: '*' }, null, 'BYE', text, false, description || 'BYE');
1308
+ // the selected mailbox is left without expunging, queued notifications are dropped
1309
+ this.closeMailbox();
1310
+ this.state = 'Logout';
1311
+ this.discardInput();
1312
+ this.end();
1313
+ }
1314
+ /**
1315
+ * Checks if the client sent anything after the command that is running, that is not processed yet
1316
+ *
1317
+ * @return {Boolean} true if there is unprocessed input or a queued command
1318
+ */
1319
+ hasPendingInput() {
1320
+ return !!(this._remainder || this._command || this._literalRemaining || this._commandQueue.length);
1321
+ }
1322
+ /**
1323
+ * Checks if a command is running or waiting, for checks that must not run ahead of earlier commands
1324
+ *
1325
+ * @return {Boolean} true if a command is running or queued
1326
+ */
1327
+ isBusy() {
1328
+ return !!(this._processing || this._commandQueue.length);
1329
+ }
1330
+ /**
1331
+ * Checks if a command was sent together with a command that was refused for the noPipelining option (STARTTLS,
1332
+ * COMPRESS): it arrived in the same read, before the client could see the refusal
1333
+ *
1334
+ * @return {Boolean} true if the command must be refused
1335
+ */
1336
+ isPipelinedAfterRefusal() {
1337
+ return !!this._pipelinedAfter && this._pipelinedAfter.read === this._readCount;
1338
+ }
1339
+ /**
1340
+ * Refuses a command that was pipelined after a refused noPipelining command, without running it
1341
+ *
1342
+ * @param {Object} parsed Parsed command
1343
+ * @param {String} data Raw command
1344
+ */
1345
+ refusePipelined(parsed, data) {
1346
+ this.sendStatus(parsed, data, 'BAD', 'Commands must not be pipelined after ' + this._pipelinedAfter?.command, false, 'INVALID COMMAND');
1347
+ }
1348
+ /**
1349
+ * Drops input that is not processed yet, including queued commands
1350
+ */
1351
+ discardInput() {
1352
+ this._commandQueue = [];
1353
+ this._remainder = '';
1354
+ this._command = '';
1355
+ this._literalRemaining = 0;
1356
+ this._skipCommand = false;
1357
+ this._earlyLiteral = false;
1358
+ }
1359
+ /**
1360
+ * Returns the connection to the Not Authenticated state and resets everything but the TLS
1361
+ * layer (RFC 8437 section 3): the selected mailbox is closed without EXPUNGE responses, and the
1362
+ * plugins clear their session state (ENABLEd extensions, CONDSTORE, COMPRESS, ...) with
1363
+ * server.resetHandlers. Call it after the response that ends the session was sent.
1364
+ */
1365
+ resetSession() {
1366
+ this.closeMailbox();
1367
+ this.state = 'Not Authenticated';
1368
+ this.username = false;
1369
+ this.everSelected = false;
1370
+ this.directNotifications = false;
1371
+ this.server.resetHandlers.forEach(handler => handler(this));
1372
+ }
1373
+ /**
1374
+ * Closes the selected mailbox, if there is one, and returns to the Authenticated state (CLOSE, UNSELECT, a failed
1375
+ * SELECT or EXAMINE, RFC 3501 section 6.3.1). The read-only mode, the \Recent set of the session and the
1376
+ * notifications that were not sent yet are dropped. Sends nothing, the caller answers the command
1377
+ */
1378
+ closeMailbox() {
1379
+ this.state = 'Authenticated';
1380
+ this.selectedMailbox = false;
1381
+ this.readOnly = false;
1382
+ this.recent = null;
1383
+ this.notificationQueue = [];
1384
+ }
1385
+ onClose() {
1386
+ if (this.socket) {
1387
+ this.socket.removeAllListeners();
1388
+ this.socket = null;
1389
+ }
1390
+ if (this.transport) {
1391
+ this.transport.destroy();
1392
+ this.transport = null;
1393
+ }
1394
+ this.server.removeListener('notify', this._notificationCallback);
1395
+ this.server.connections.delete(this);
1396
+ }
1397
+ onError(err) {
1398
+ if (this.options.debug) {
1399
+ console.log('Socket error event emitted, %s', Date());
1400
+ console.log(err.stack);
1401
+ }
1402
+ try {
1403
+ this.socket?.end();
1404
+ }
1405
+ catch (E) {
1406
+ // socket is already gone
1407
+ }
1408
+ }
1409
+ onData(chunk) {
1410
+ let match;
1411
+ let str;
1412
+ // everything in one read arrived before anything this read causes to be sent
1413
+ this._readCount = (this._readCount || 0) + 1;
1414
+ str = (chunk || '').toString('binary');
1415
+ if (this._discardLine) {
1416
+ // skipping the rest of a command line that was too long
1417
+ const lineEnd = str.indexOf('\n');
1418
+ if (lineEnd < 0) {
1419
+ return;
1420
+ }
1421
+ // the command was not executed, but its tag still gets an answer
1422
+ const tag = this._discardLine;
1423
+ this._discardLine = false;
1424
+ str = str.substr(lineEnd + 1);
1425
+ this.sendBad(tag, 'Command line too long', 'LINE TOO LONG');
1426
+ }
1427
+ if (this._literalRemaining) {
1428
+ str = this.readLiteral(str);
1429
+ if (this._literalRemaining) {
1430
+ return;
1431
+ }
1432
+ }
1433
+ // non-synchronizing literals are only valid when LITERAL+ or LITERAL- is advertised. A literal8 marker `~{n}`
1434
+ // (RFC 3516) is matched always, so it is refused when BINARY is not loaded
1435
+ const lineEndRegex = this.server.literalPlus
1436
+ ? /(?<marker>(?<tilde>~)?\{(?<size>\d+)(?<plus>\+)?\})?(?<cr>\r?)\n/
1437
+ : /(?<marker>(?<tilde>~)?\{(?<size>\d+)\})?(?<cr>\r?)\n/;
1438
+ this._remainder = str = this._remainder + str;
1439
+ while ((match = lineEndRegex.exec(str))) {
1440
+ const { marker, tilde, size, plus, cr } = match.groups;
1441
+ if (match.index > MAX_LINE_LENGTH) {
1442
+ // the same limit as for a line that arrives in several reads (see the end of this method), so
1443
+ // the outcome does not depend on how the input was split into reads (RFC 3501 section 7.1.3)
1444
+ const tag = getResponseTag(this._command || str);
1445
+ this.sendBad('*', 'Command line too long', 'LINE TOO LONG');
1446
+ this.sendBad(tag, 'Command line too long', 'LINE TOO LONG');
1447
+ this._remainder = str = str.substr(match.index + match[0].length);
1448
+ this._command = '';
1449
+ this._skipCommand = false;
1450
+ continue;
1451
+ }
1452
+ if (!cr) {
1453
+ // every command line ends with CRLF (RFC 3501 section 9), a bare LF is refused
1454
+ const line = this._command + str.substr(0, match.index + match[0].length - 1);
1455
+ this._remainder = str = str.substr(match.index + match[0].length);
1456
+ this._command = '';
1457
+ if (this._skipCommand) {
1458
+ this._skipCommand = false;
1459
+ }
1460
+ else {
1461
+ this.sendBad(this.inputHandler ? '*' : getResponseTag(line), 'Lines must end with CRLF', 'INVALID LINE ENDING', line);
1462
+ }
1463
+ continue;
1464
+ }
1465
+ if (!size || (this._skipCommand && !plus)) {
1466
+ // the command is complete, or it was refused already and the client waits in vain
1467
+ // for a continuation request
1468
+ const line = this._command + str.substr(0, match.index);
1469
+ this._remainder = str.substr(match.index + match[0].length);
1470
+ this._command = '';
1471
+ if (this._skipCommand) {
1472
+ this._skipCommand = false;
1473
+ }
1474
+ else if (this._earlyLiteral) {
1475
+ // the client sent literal data without waiting for the continuation request
1476
+ this._earlyLiteral = false;
1477
+ this.sendBad(getResponseTag(line), 'Literal data must wait for the continuation request', 'LITERAL TOO EARLY', line);
1478
+ }
1479
+ else if (this.inputHandler) {
1480
+ this.inputHandler(line);
1481
+ }
1482
+ else {
1483
+ this.scheduleCommand(line);
1484
+ }
1485
+ if (this.upgrading) {
1486
+ // STARTTLS was accepted, ignore any pipelined plaintext input
1487
+ return;
1488
+ }
1489
+ // a handler may have dropped the input that followed with discardInput()
1490
+ str = this._remainder;
1491
+ continue;
1492
+ }
1493
+ const literalSize = Number(size);
1494
+ if (!this._skipCommand) {
1495
+ const line = this._command + str.substr(0, match.index);
1496
+ // the literal marker is part of the first word when it directly follows the tag
1497
+ const tag = getResponseTag(line + marker);
1498
+ const refusal = this.checkLiteral(line, literalSize, !plus, !!tilde);
1499
+ if (refusal) {
1500
+ if (plus) {
1501
+ // the client is going to send the literal anyway, so there is no way to recover. A BYE
1502
+ // for a literal that is too large should carry TOOBIG (RFC 7888 section 5)
1503
+ this.sendStatus({ tag: '*' }, line, 'BYE', refusal.text, refusal.text === LITERAL_TOO_LARGE && 'TOOBIG', 'LITERAL REFUSED');
1504
+ this._remainder = this._command = '';
1505
+ this.end();
1506
+ return;
1507
+ }
1508
+ // refuse a synchronizing literal by not sending a continuation request
1509
+ this.sendStatus({ tag }, line, refusal.command, refusal.text, refusal.code || false, 'LITERAL REFUSED');
1510
+ this._remainder = str = str.substr(match.index + match[0].length);
1511
+ this._command = '';
1512
+ continue;
1513
+ }
1514
+ if (plus && literalSize > this.server.nonSyncLiteralLimit) {
1515
+ // RFC 7888 sections 4 and 5: the command is refused with TOOBIG, the literal and the
1516
+ // rest of the command are read and dropped
1517
+ this.sendStatus({ tag }, line, 'BAD', 'Non-synchronizing literals are limited to ' + this.server.nonSyncLiteralLimit + ' octets', 'TOOBIG', 'LITERAL TOO BIG');
1518
+ this._skipCommand = true;
1519
+ this._command = '';
1520
+ }
1521
+ }
1522
+ if (!plus) {
1523
+ if (str.length > match.index + match[0].length) {
1524
+ // RFC 3501 section 4.3: the client MUST wait for the continuation request
1525
+ // before sending the octets of a synchronizing literal
1526
+ this._earlyLiteral = true;
1527
+ }
1528
+ else if (!this._earlyLiteral) {
1529
+ this.write('+ Go ahead\r\n');
1530
+ }
1531
+ }
1532
+ this._remainder = '';
1533
+ if (!this._skipCommand) {
1534
+ this._command += str.substr(0, match.index + match[0].length);
1535
+ }
1536
+ this._literalRemaining = literalSize;
1537
+ str = this.readLiteral(str.substr(match.index + match[0].length));
1538
+ if (this._literalRemaining) {
1539
+ return;
1540
+ }
1541
+ this._remainder = str;
1542
+ }
1543
+ if (this._remainder.length > MAX_LINE_LENGTH) {
1544
+ // RFC 3501 section 7.1.3
1545
+ this.sendBad('*', 'Command line too long', 'LINE TOO LONG');
1546
+ const tag = getResponseTag(this._command || this._remainder);
1547
+ this._remainder = '';
1548
+ this._command = '';
1549
+ this._skipCommand = false;
1550
+ this._discardLine = tag;
1551
+ }
1552
+ }
1553
+ /**
1554
+ * Reads literal data that the current command is waiting for. The data of a command that was
1555
+ * refused is dropped.
1556
+ *
1557
+ * @param {String} str Received data
1558
+ * @return {String} the data that follows the literal
1559
+ */
1560
+ readLiteral(str) {
1561
+ const length = Math.min(this._literalRemaining, str.length);
1562
+ if (!this._skipCommand) {
1563
+ this._command += str.substr(0, length);
1564
+ }
1565
+ this._literalRemaining -= length;
1566
+ return str.substr(length);
1567
+ }
1568
+ /**
1569
+ * Sends a BAD response to input that did not make it to a command handler
1570
+ *
1571
+ * @param {String} tag Tag to answer with, "*" for an untagged response
1572
+ * @param {String} text Human readable text
1573
+ * @param {String} description Description for output handlers
1574
+ * @param {String} [data] Raw input
1575
+ */
1576
+ sendBad(tag, text, description, data) {
1577
+ this.sendStatus({ tag }, data, 'BAD', text, false, description);
1578
+ }
1579
+ /**
1580
+ * Checks if a literal may be accepted for the command line received so far. Literals are
1581
+ * refused before they are read when they are too large, or when the command is unknown or
1582
+ * not allowed in the current state, so the client does not get a continuation request for
1583
+ * a command that is going to fail anyway.
1584
+ *
1585
+ * @param {String} line The command received so far (with earlier literals), up to the literal size marker
1586
+ * @param {Number} literalSize Size of the literal in octets
1587
+ * @param {Boolean} [synchronizing] true if the literal can still be refused without reading it
1588
+ * @param {Boolean} [literal8] The literal is a literal8 `~{n}`
1589
+ * @return {Object|Boolean} `{ command, code, text }` for the response that refuses the literal, or false.
1590
+ * Responses from `server.literalFilters` have the same form
1591
+ */
1592
+ checkLiteral(line, literalSize, synchronizing, literal8) {
1593
+ const refuse = (text) => ({ command: 'BAD', text });
1594
+ const maxLiteralSize = this.getMaxLiteralSize();
1595
+ if (literalSize > maxLiteralSize || line.length + literalSize > maxLiteralSize + MAX_LINE_LENGTH) {
1596
+ return refuse(LITERAL_TOO_LARGE);
1597
+ }
1598
+ if (this.isPipelinedAfterRefusal()) {
1599
+ return refuse('Commands must not be pipelined after ' + this._pipelinedAfter?.command);
1600
+ }
1601
+ if (this.inputHandler) {
1602
+ // not a command, e.g. a SASL response
1603
+ return literal8 ? refuse('Literal8 is not allowed here') : false;
1604
+ }
1605
+ // tag SP command, and for UID and AUTHENTICATE the word that follows
1606
+ const words = line.match(/^[^ ]* ([^ ]*)(?: ([^ ]*))?/);
1607
+ let command = ((words && words[1]) || '').toUpperCase();
1608
+ if (command === 'UID' || command === 'AUTHENTICATE') {
1609
+ command += ' ' + ((words && words[2]) || '').toUpperCase();
1610
+ }
1611
+ if (!COMMAND_REGEX.test(command) || !this.server.getCommandHandler(command)) {
1612
+ return refuse('Unknown command');
1613
+ }
1614
+ const options = this.server.getCommandOptions(command);
1615
+ if (options.states && options.states.indexOf(this.state) < 0) {
1616
+ return refuse(stateError(command, this.state));
1617
+ }
1618
+ // RFC 3516 section 7: literal8 is only valid where an extension allows it, like BINARY for the APPEND
1619
+ // message or METADATA for entry values (RFC 5464 section 5)
1620
+ if (literal8 && !(options.literal8 === true || (options.literal8 && Object.hasOwn(this.server.capabilities, options.literal8)))) {
1621
+ return refuse('Literal8 is not allowed in ' + command);
1622
+ }
1623
+ // plugins can refuse a synchronizing literal, e.g. a message that is too large for APPEND.
1624
+ // A non-synchronizing literal is read anyway, the command handler refuses it later
1625
+ if (synchronizing) {
1626
+ for (const filter of this.server.literalFilters) {
1627
+ const refusal = filter(this, command, line, literalSize);
1628
+ if (refusal) {
1629
+ return refusal;
1630
+ }
1631
+ }
1632
+ }
1633
+ return false;
1634
+ }
1635
+ /**
1636
+ * Returns the largest literal the client may send in its current state. Before
1637
+ * authentication only small literals (user names, passwords) make sense.
1638
+ *
1639
+ * @return {Number} Size in bytes
1640
+ */
1641
+ getMaxLiteralSize() {
1642
+ if (this.state === 'Not Authenticated') {
1643
+ return MAX_PREAUTH_LITERAL_SIZE;
1644
+ }
1645
+ return Number(this.options.maxLiteralSize) || MAX_LITERAL_SIZE;
1646
+ }
1647
+ /**
1648
+ * Returns the message list of the selected mailbox as this session currently
1649
+ * sees it. When another session has expunged messages that this session has
1650
+ * not been told about yet, sequence numbers must still refer to the old list.
1651
+ *
1652
+ * @return {Array} List of messages
1653
+ */
1654
+ getSessionMessages() {
1655
+ for (let i = 0, len = this.notificationQueue.length; i < len; i++) {
1656
+ const mailboxCopy = this.notificationQueue[i].mailboxCopy;
1657
+ if (mailboxCopy) {
1658
+ return mailboxCopy;
1659
+ }
1660
+ }
1661
+ return this.selectedMailbox ? this.selectedMailbox.messages : [];
1662
+ }
1663
+ /**
1664
+ * Resolves the sequence set argument of a command to messages of the selected mailbox, as this
1665
+ * session sees it. Plugins can replace it per connection to support other forms of sequence sets
1666
+ * (e.g. "$" of SEARCHRES)
1667
+ *
1668
+ * @param {String} range Sequence set
1669
+ * @param {Boolean} isUid If true, the set lists UIDs instead of sequence numbers
1670
+ * @return {Array} An array of messages in the form of [[seqIndex, message]]
1671
+ */
1672
+ getMessageRange(range, isUid) {
1673
+ return this.server.getMessageRange(this.getSessionMessages(), range, isUid);
1674
+ }
1675
+ /**
1676
+ * Lets `server.rangeLimits` cut the messages a command operates on, after its sequence set argument was resolved.
1677
+ * A limit that returns the messages from the highest UID down sets `parsed.highestFirst`, then MOVE and UID EXPUNGE
1678
+ * send their EXPUNGE responses in that order too
1679
+ *
1680
+ * @param {Object} parsed Parsed command
1681
+ * @param {Array} range Messages of the sequence set, in the form of [[seqIndex, message]]
1682
+ * @return {Array} the messages to operate on, in the same form
1683
+ */
1684
+ limitRange(parsed, range) {
1685
+ return this.server.rangeLimits.reduce((result, limit) => limit(this, parsed, result) || result, range);
1686
+ }
1687
+ /**
1688
+ * Resolves the sequence set argument of a command like getMessageRange, but refuses message sequence
1689
+ * numbers past the end of the selected mailbox, as this session sees it. RFC 3501 and RFC 9051 section 9
1690
+ * (seq-number): "The server should respond with a tagged BAD response to a command that uses a message
1691
+ * sequence number greater than the number of messages in the selected mailbox. This includes "*" if the
1692
+ * selected mailbox is empty." For the sequence set argument of FETCH, STORE, COPY and MOVE, SEARCH keys
1693
+ * use getMessageRange
1694
+ *
1695
+ * @param {String} range Sequence set
1696
+ * @param {Boolean} isUid If true, the set lists UIDs, these can point past the end
1697
+ * @return {Array} An array of messages in the form of [[seqIndex, message]], see getMessageRange
1698
+ * @throws {Error} BAD error if a sequence number is out of range
1699
+ */
1700
+ getCommandRange(range, isUid) {
1701
+ const result = this.getMessageRange(range, isUid);
1702
+ if (isUid) {
1703
+ return result;
1704
+ }
1705
+ const total = this.getSessionMessages().length;
1706
+ String(range)
1707
+ .split(/[,:]/)
1708
+ .forEach(value => {
1709
+ if (value === '*' ? !total : Number(value) > total) {
1710
+ const err = new Error(total ? 'Message sequence number ' + value + ' is greater than the number of messages (' + total + ')' : 'The mailbox is empty');
1711
+ err.imapResponse = 'BAD';
1712
+ throw err;
1713
+ }
1714
+ });
1715
+ return result;
1716
+ }
1717
+ /**
1718
+ * Checks if this session has EXPUNGE notifications that it has not been told about yet
1719
+ *
1720
+ * @return {Boolean} true if an EXPUNGE response is pending
1721
+ */
1722
+ hasPendingExpunge() {
1723
+ return this.notificationQueue.some(notification => notification.attributes && (notification.attributes[1] || {}).value === 'EXPUNGE');
1724
+ }
1725
+ /**
1726
+ * Tells the other sessions that have the selected mailbox open about changed flags, they get
1727
+ * an untagged FETCH with the new flags (RFC 3501 section 5.2)
1728
+ *
1729
+ * @param {Array} messages Messages with changed flags
1730
+ */
1731
+ notifyFlagChanges(messages) {
1732
+ if (messages.length && this.selectedMailbox) {
1733
+ this.server.notify({ tag: '*', flagUpdate: messages }, this.selectedMailbox, this);
1734
+ }
1735
+ }
1736
+ /**
1737
+ * Sends unsolicited FETCH responses with the flags another session changed. The UID is always
1738
+ * included, RFC 9051 section 6.3.13 requires it for unsolicited FETCH responses and it is valid
1739
+ * in IMAP4rev1 as well.
1740
+ *
1741
+ * @param {Array} messages Messages with changed flags
1742
+ * @param {Map} sequence Message to the sequence number this session knows it by
1743
+ * @param {Set} [reported] Messages already reported, these are skipped and the sent ones are added
1744
+ */
1745
+ sendFlagUpdate(messages, sequence, reported) {
1746
+ const getFlags = this.server.fetchHandlers.FLAGS || fetchHandlers.FLAGS;
1747
+ messages.forEach(message => {
1748
+ if (!sequence.has(message) || message.ghost) {
1749
+ // the message is gone, its EXPUNGE response tells the rest
1750
+ return;
1751
+ }
1752
+ if (reported) {
1753
+ if (reported.has(message)) {
1754
+ return;
1755
+ }
1756
+ reported.add(message);
1757
+ }
1758
+ this.send({
1759
+ tag: '*',
1760
+ notification: true,
1761
+ attributes: [
1762
+ sequence.get(message),
1763
+ {
1764
+ type: 'ATOM',
1765
+ value: 'FETCH'
1766
+ },
1767
+ [
1768
+ {
1769
+ type: 'ATOM',
1770
+ value: 'UID'
1771
+ },
1772
+ message.uid,
1773
+ {
1774
+ type: 'ATOM',
1775
+ value: 'FLAGS'
1776
+ },
1777
+ getFlags(this, message, { type: 'ATOM', value: 'FLAGS' })
1778
+ ]
1779
+ ]
1780
+ }, 'FLAG NOTIFICATION', null, null, message);
1781
+ });
1782
+ }
1783
+ /**
1784
+ * Checks if a message has the \Recent flag in this session
1785
+ *
1786
+ * @param {Object} message Message object
1787
+ * @return {Boolean} true if the message is recent for this session
1788
+ */
1789
+ isRecent(message) {
1790
+ return !!(this.recent && this.recent.has(message));
1791
+ }
1792
+ /**
1793
+ * Returns the flags of a message as seen by this session, including \Recent
1794
+ *
1795
+ * @param {Object} message Message object
1796
+ * @return {Array} List of flags
1797
+ */
1798
+ getFlags(message) {
1799
+ return this.isRecent(message) ? message.flags.concat('\\Recent') : message.flags;
1800
+ }
1801
+ /**
1802
+ * Checks if FETCH may set the \Seen flag in the selected mailbox (RFC 3501 section 6.4.5).
1803
+ * Plugins can override it for a connection, e.g. ACL without the "s" right
1804
+ *
1805
+ * @return {Boolean} true if \Seen may be set
1806
+ */
1807
+ canSetSeen() {
1808
+ return !this.readOnly;
1809
+ }
1810
+ /**
1811
+ * The refusal of a command that would change a mailbox selected read-only (EXAMINE, or SELECT answered with
1812
+ * [READ-ONLY], RFC 3501 sections 6.3.1 and 6.3.2): STORE, EXPUNGE, UID EXPUNGE, MOVE and REPLACE. RFC 5530 section 3:
1813
+ * CLIENTBUG, the client was told that the mailbox is read-only
1814
+ *
1815
+ * @return {Object|Boolean} `{ command, code, text }` if the selected mailbox is read-only, otherwise false
1816
+ */
1817
+ readOnlyRefusal() {
1818
+ return this.readOnly ? { command: 'NO', code: 'CLIENTBUG', text: 'Mailbox is read-only' } : false;
1819
+ }
1820
+ /**
1821
+ * Sends the refusal of readOnlyRefusal() if the selected mailbox is read-only
1822
+ *
1823
+ * @param {Object} parsed Parsed command
1824
+ * @param {String} data Raw command
1825
+ * @param {String} description Description for output handlers
1826
+ * @return {Boolean} true if the command was refused
1827
+ */
1828
+ refuseReadOnly(parsed, data, description) {
1829
+ const refusal = this.readOnlyRefusal();
1830
+ if (refusal) {
1831
+ this.sendStatus(parsed, data, refusal.command, refusal.text, refusal.code, description);
1832
+ }
1833
+ return !!refusal;
1834
+ }
1835
+ /**
1836
+ * Checks if CLOSE may expunge the selected mailbox (RFC 3501 section 6.4.2). Plugins can
1837
+ * override it for a connection, e.g. ACL without the "e" right
1838
+ *
1839
+ * @return {Boolean} true if messages may be expunged
1840
+ */
1841
+ canExpunge() {
1842
+ return !this.readOnly;
1843
+ }
1844
+ onNotify(notification) {
1845
+ if (notification.ignoreConnection === this ||
1846
+ (notification.filter && !notification.filter(this)) ||
1847
+ !this.server.notifyFilters.every(filter => filter(this, notification))) {
1848
+ return;
1849
+ }
1850
+ const mailbox = typeof notification.mailbox === 'string' ? this.server.getMailbox(notification.mailbox) : notification.mailbox;
1851
+ if (!notification.mailbox || (this.selectedMailbox && this.selectedMailbox === mailbox)) {
1852
+ let command = notification.command;
1853
+ if (command.mailboxCopy && this.notificationQueue.some(queued => queued.mailboxCopy)) {
1854
+ // Only the oldest snapshot describes what this session currently sees,
1855
+ // so do not keep another copy of the message list around
1856
+ command = Object.assign({}, command);
1857
+ delete command.mailboxCopy;
1858
+ }
1859
+ this.queueNotification(command, notification);
1860
+ }
1861
+ }
1862
+ /**
1863
+ * Queues a notification for this session, it is sent before the next tagged response that allows it, or
1864
+ * right away while notifications are direct (IDLE). Plugins can replace it per connection to drop
1865
+ * notifications or send them at other times (e.g. NOTIFY)
1866
+ *
1867
+ * @param {Object} command Untagged response
1868
+ * @param {Object} notification The `notify` event, `{ command, mailbox, ignoreConnection, filter, origin }`
1869
+ */
1870
+ queueNotification(command, notification) {
1871
+ this.notificationQueue.push(command);
1872
+ if (this.directNotifications) {
1873
+ this.processNotifications();
1874
+ }
1875
+ }
1876
+ upgradeConnection(callback) {
1877
+ this.upgrading = true;
1878
+ // Anything the client sent after STARTTLS in plaintext must not be executed after the upgrade (RFC 9051
1879
+ // section 6.2.1). STARTTLS is refused when input is waiting (the noPipelining command option), this only
1880
+ // guards against plugins that upgrade the connection otherwise
1881
+ this.discardInput();
1882
+ const secureContext = tls.createSecureContext(this.server.getCredentials());
1883
+ const socketOptions = {
1884
+ secureContext: secureContext,
1885
+ isServer: true,
1886
+ server: this.server.server,
1887
+ // throws if SNICallback is missing, so we set a default callback
1888
+ SNICallback: function (servername, cb) {
1889
+ cb(null, secureContext);
1890
+ }
1891
+ };
1892
+ // STARTTLS runs on a live connection
1893
+ const socket = this.socket;
1894
+ // remove all listeners from the original socket besides the error handler
1895
+ socket.removeAllListeners();
1896
+ socket.on('error', this.onError.bind(this));
1897
+ // upgrade connection
1898
+ const secureSocket = new tls.TLSSocket(socket, socketOptions);
1899
+ const onTLSError = (err) => {
1900
+ // a failed handshake leaves nothing to talk to, so drop the connection
1901
+ if (this.options.debug) {
1902
+ console.log('TLS error: %s', err.message);
1903
+ }
1904
+ secureSocket.destroy();
1905
+ };
1906
+ secureSocket.on('close', this.onClose.bind(this));
1907
+ secureSocket.on('error', onTLSError);
1908
+ secureSocket.on('clientError', onTLSError);
1909
+ secureSocket.on('secure', () => {
1910
+ this.secureConnection = true;
1911
+ this.socket = secureSocket;
1912
+ this.upgrading = false;
1913
+ secureSocket.on('data', this.receive.bind(this));
1914
+ callback();
1915
+ });
1916
+ }
1917
+ /**
1918
+ * Turns the queued notifications into the responses to send. Plugins can replace it per connection
1919
+ * to report changes in another form (e.g. VANISHED instead of EXPUNGE with QRESYNC)
1920
+ *
1921
+ * @param {Array} queue Queued notifications
1922
+ * @return {Array} Notifications to send
1923
+ */
1924
+ prepareNotifications(queue) {
1925
+ return queue;
1926
+ }
1927
+ processNotifications(data) {
1928
+ const options = data && this.server.getCommandOptions(data.command);
1929
+ if (options && (options.noExpunge || (options.searchCriteria !== false && this.usesSequenceNumbers(data)))) {
1930
+ // EXPUNGE responses are not allowed during FETCH, STORE and SEARCH (RFC 3501 section 7.4.1), during
1931
+ // the commands that extensions add to this list (see the noExpunge command option), nor during UID
1932
+ // SEARCH with message numbers in the search criteria (RFC 7162 section 3.2.10.2 for VANISHED, EXPUNGE
1933
+ // may wait as well, RFC 3501 only allows it during UID commands)
1934
+ return;
1935
+ }
1936
+ if (!this.notificationQueue.length) {
1937
+ return;
1938
+ }
1939
+ const queue = this.prepareNotifications(this.notificationQueue);
1940
+ this.notificationQueue = [];
1941
+ // Flag updates use the sequence numbers this session knows: before the EXPUNGE responses of
1942
+ // the snapshot are sent, the snapshot, afterwards the current message list
1943
+ const snapshotIndex = queue.findIndex(notification => notification.mailboxCopy);
1944
+ const sequenceMaps = new Map();
1945
+ const getSequence = (messages) => {
1946
+ let sequence = sequenceMaps.get(messages);
1947
+ if (!sequence) {
1948
+ sequence = new Map(messages.map((message, i) => [message, i + 1]));
1949
+ sequenceMaps.set(messages, sequence);
1950
+ }
1951
+ return sequence;
1952
+ };
1953
+ const current = this.selectedMailbox ? this.selectedMailbox.messages : [];
1954
+ // a message changed several times is reported once, its FETCH response carries the current flags
1955
+ const reported = new Set();
1956
+ queue.forEach((notification, i) => {
1957
+ if (notification.flagUpdate) {
1958
+ // i < snapshotIndex only when there is a snapshot
1959
+ this.sendFlagUpdate(notification.flagUpdate, getSequence(i < snapshotIndex ? queue[snapshotIndex].mailboxCopy : current), reported);
1960
+ }
1961
+ else {
1962
+ this.send(notification);
1963
+ }
1964
+ });
1965
+ }
1966
+ /**
1967
+ * Compile a command object to a response string and write it to socket.
1968
+ * If the command object has a skipResponse property, the command is
1969
+ * ignored
1970
+ *
1971
+ * @param {Object} response Response IMAP command object to be compiled.
1972
+ * @param {String} description
1973
+ * An upper-case string uniquely identifying the response for the benefit of
1974
+ * output handlers that wish to augment/replace the given response.
1975
+ * @param {Object} parsed
1976
+ * Original parsed IMAP command that this is in response to.
1977
+ * @param {String} data
1978
+ * Original raw IMAP command as a binary string.
1979
+ * @param {Object} extra
1980
+ * Response-specific payload, usually the subject of the response. For
1981
+ * example, the STORE command will pass the impacted message for each updated
1982
+ * FETCH result. (This may have other names when used, like "affected".)
1983
+ */
1984
+ send(response, description, parsed, data, ...extra) {
1985
+ // nothing goes out once the connection is closing (RFC 3501 section 7.1.5)
1986
+ if (!this.socket || this.socket.destroyed || this._closing) {
1987
+ return;
1988
+ }
1989
+ if (!response.notification && response.tag !== '*') {
1990
+ // arguments[2] should be the original command
1991
+ this.processNotifications(parsed);
1992
+ }
1993
+ this.server.outputHandlers.forEach(handler => {
1994
+ handler(this, response, description, parsed, data, ...extra);
1995
+ });
1996
+ // No need to display this response to user
1997
+ if (response.skipResponse) {
1998
+ return;
1999
+ }
2000
+ if (this.notificationQueue.length &&
2001
+ parsed &&
2002
+ response.tag === parsed.tag &&
2003
+ response.command === 'OK' &&
2004
+ this.hasPendingExpunge() &&
2005
+ this.server.getCommandOptions(parsed.command).noExpunge &&
2006
+ !(Array.isArray(response.attributes) && response.attributes.some(attr => attr && attr.type === 'SECTION'))) {
2007
+ // After the output handlers, they might add a response code of their own (MODIFIED of CONDSTORE).
2008
+ // FETCH, STORE, SEARCH and the like can not report the EXPUNGE of another session (RFC 3501 section 7.4.1),
2009
+ // EXPUNGEISSUED tells the client to issue NOOP soon (RFC 5530 section 3, RFC 9051 section 7.1)
2010
+ response.attributes = [{ type: 'SECTION', section: [{ type: 'ATOM', value: 'EXPUNGEISSUED' }] }].concat(response.attributes || []);
2011
+ }
2012
+ // a { type: 'MAILBOX', value } attribute holds a storage name, sent in the form this session uses. It
2013
+ // can also be in a list, like the MAILBOX correlator of an ESEARCH response (RFC 7377 section 4)
2014
+ const isMailbox = (attr) => attr && attr.type === 'MAILBOX';
2015
+ const hasMailbox = (list) => list.some((attr) => isMailbox(attr) || (Array.isArray(attr) && hasMailbox(attr)));
2016
+ const exportList = (list) => list.map((attr) => isMailbox(attr) ? mailboxAttribute(this.exportMailboxName(attr.value)) : Array.isArray(attr) ? exportList(attr) : attr);
2017
+ if (Array.isArray(response.attributes) && hasMailbox(response.attributes)) {
2018
+ response = Object.assign({}, response, { attributes: exportList(response.attributes) });
2019
+ }
2020
+ // RFC 3501 section 9: TEXT-CHAR is 7-bit (CHAR = %x01-7F), so client input echoed in the
2021
+ // human readable text of a status response must not carry 8-bit or control octets
2022
+ if (STATUS_RESPONSES.has((response.command || '').toString().toUpperCase()) && Array.isArray(response.attributes)) {
2023
+ const isUnsafe = (attr) => attr && attr.type === 'TEXT' && typeof attr.value === 'string' && /[^\x20-\x7e]/.test(attr.value);
2024
+ if (response.attributes.some(isUnsafe)) {
2025
+ response = Object.assign({}, response, {
2026
+ attributes: response.attributes.map((attr) => isUnsafe(attr) ? Object.assign({}, attr, { value: attr.value.replace(/[^\x20-\x7e]/g, '?') }) : attr)
2027
+ });
2028
+ }
2029
+ }
2030
+ let compiled;
2031
+ try {
2032
+ compiled = imapHandler.compiler(response, this.compilerOptions);
2033
+ }
2034
+ catch (err) {
2035
+ // the compiler refuses unsafe output, like line breaks in a TEXT value
2036
+ if (this.options.debug) {
2037
+ console.log('Failed to compile response: %s', err.message);
2038
+ }
2039
+ if (response.tag === '*') {
2040
+ return;
2041
+ }
2042
+ compiled = response.tag + ' NO [SERVERBUG] Failed to compile response';
2043
+ }
2044
+ if (this.options.debug) {
2045
+ console.log('SEND: %s', compiled);
2046
+ }
2047
+ this.write(compiled + '\r\n');
2048
+ }
2049
+ /**
2050
+ * Sends a tagged status response to a command
2051
+ *
2052
+ * @param {Object} parsed Parsed command
2053
+ * @param {String} data Raw command
2054
+ * @param {String} command Response type: OK, NO or BAD
2055
+ * @param {String} text Human readable text
2056
+ * @param {String|Array} [code] Response code, eg. "TRYCREATE", sent as [TRYCREATE], or a list of
2057
+ * atoms like ["METADATA", "MAXSIZE", 1024], sent as [METADATA MAXSIZE 1024]
2058
+ * @param {String} [description] Description for output handlers, defaults to the command name,
2059
+ * with " FAILED" appended for NO and BAD
2060
+ */
2061
+ sendStatus(parsed, data, command, text, code, description) {
2062
+ const attributes = [];
2063
+ if (code) {
2064
+ attributes.push({
2065
+ type: 'SECTION',
2066
+ section: [].concat(code).map(value => ({
2067
+ type: 'ATOM',
2068
+ value: String(value)
2069
+ }))
2070
+ });
2071
+ }
2072
+ attributes.push({
2073
+ type: 'TEXT',
2074
+ value: text
2075
+ });
2076
+ if (!description) {
2077
+ description = (parsed.command || '').toString().toUpperCase() + (command === 'OK' ? '' : ' FAILED');
2078
+ }
2079
+ this.send({
2080
+ tag: parsed.tag,
2081
+ command,
2082
+ attributes
2083
+ }, description, parsed, data);
2084
+ }
2085
+ /**
2086
+ * Checks if a command was sent without waiting for an earlier command in a way that RFC 3501
2087
+ * section 5.5 forbids: after any command other than FETCH, STORE or SEARCH (or another command with
2088
+ * the noExpunge option, like SORT and THREAD from RFC 5256) the client must
2089
+ * wait for the completion result before it sends a command with message sequence numbers,
2090
+ * because an EXPUNGE response could change what the numbers refer to. A command was sent
2091
+ * without waiting if such a command is still queued or running, or if it completed in the
2092
+ * same read as this command arrived in.
2093
+ *
2094
+ * @param {Object} parsed Parsed command
2095
+ * @return {Boolean} true if the command is ambiguous
2096
+ */
2097
+ isAmbiguous(parsed) {
2098
+ if (!this.usesSequenceNumbers(parsed)) {
2099
+ return false;
2100
+ }
2101
+ if (this._unsafeCompletedRead === this._readCount) {
2102
+ return true;
2103
+ }
2104
+ const isUnsafe = (element) => !!element && !this.server.getCommandOptions(element.parsed.command).noExpunge;
2105
+ return isUnsafe(this._runningCommand) || this._commandQueue.some(isUnsafe);
2106
+ }
2107
+ /**
2108
+ * Checks if a command refers to messages by sequence number (RFC 3501 section 5.5)
2109
+ *
2110
+ * @param {Object} parsed Parsed command
2111
+ * @return {Boolean} true if the command uses message sequence numbers
2112
+ */
2113
+ usesSequenceNumbers(parsed) {
2114
+ const { sequenceSet, searchCriteria } = this.server.getCommandOptions(parsed.command);
2115
+ if (sequenceSet !== false) {
2116
+ // other forms of sequence sets, like "$" of SEARCHRES (RFC 5182 section 2.3), do not use numbers
2117
+ const value = parsed.attributes && parsed.attributes[sequenceSet];
2118
+ return !value || /^[\d*]/.test(String(value.value));
2119
+ }
2120
+ if (searchCriteria !== false) {
2121
+ return hasSequenceSetKey(this.server, (parsed.attributes || []).slice(searchCriteria));
2122
+ }
2123
+ return false;
2124
+ }
2125
+ /**
2126
+ * Decodes a SASL client response. It must be valid base64 by the RFC 3501 section 9 grammar,
2127
+ * "=" stands for an empty initial response (RFC 4959 section 3).
2128
+ *
2129
+ * @param {String} str Client response
2130
+ * @return {Buffer|Boolean} Decoded value, or false if the input is not valid base64
2131
+ */
2132
+ decodeSaslResponse(str) {
2133
+ if (str === '=') {
2134
+ return Buffer.alloc(0);
2135
+ }
2136
+ if (typeof str !== 'string' || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(str)) {
2137
+ return false;
2138
+ }
2139
+ return Buffer.from(str, 'base64');
2140
+ }
2141
+ /**
2142
+ * Returns the target mailbox of APPEND, COPY or MOVE. If messages can not be added
2143
+ * to it, a tagged NO is sent and false is returned
2144
+ *
2145
+ * @param {String} path Mailbox path
2146
+ * @param {Object} parsed Parsed command
2147
+ * @param {String} data Raw command
2148
+ * @param {String} description Description for the failure response
2149
+ * @return {Object|false} Mailbox object
2150
+ */
2151
+ getTargetMailbox(path, parsed, data, description) {
2152
+ const refusal = this.server.targetRefusal(path);
2153
+ if (refusal) {
2154
+ this.sendStatus(parsed, data, refusal.command, refusal.text, refusal.code, description);
2155
+ return false;
2156
+ }
2157
+ // targetRefusal() found it
2158
+ return this.server.getMailbox(path);
2159
+ }
2160
+ /**
2161
+ * Runs the checks of `server.appendChecks` before messages are added to a mailbox. A check
2162
+ * returns nothing if the messages may be added, or `{ code, text, soft }`: a hard failure is
2163
+ * sent as a tagged NO with the response code and false is returned, a soft one (`soft: true`)
2164
+ * only as an untagged NO warning, e.g. `* NO [OVERQUOTA] ...` (RFC 9208 section 4.3.1).
2165
+ *
2166
+ * @param {Object} mailbox Target mailbox
2167
+ * @param {Array} messages Messages to add, objects with the message source as `raw`
2168
+ * @param {Object} parsed Parsed command
2169
+ * @param {String} data Raw command
2170
+ * @param {String} description Description for the failure response
2171
+ * @param {Object} [options] `{ move, source }`, set for MOVE with the source mailbox, or `{ command, replaced }`
2172
+ * for APPEND like commands (APPEND, REPLACE), `replaced` is the message that REPLACE removes
2173
+ * @return {Boolean} true if the messages may be added
2174
+ */
2175
+ checkAppend(mailbox, messages, parsed, data, description, options) {
2176
+ return this.applyChecks(this.server.appendChecks.map(check => check(this, mailbox, messages, options || {})), parsed, data, description);
2177
+ }
2178
+ /**
2179
+ * Reports the results of checks like `server.appendChecks`: the first hard failure as a tagged
2180
+ * NO, or soft ones as untagged NO warnings
2181
+ *
2182
+ * @param {Array} results Check results, `{ code, text, soft }` or nothing
2183
+ * @param {Object} parsed Parsed command
2184
+ * @param {String} data Raw command
2185
+ * @param {String} description Description for the failure response
2186
+ * @return {Boolean} false if the command failed
2187
+ */
2188
+ applyChecks(results, parsed, data, description) {
2189
+ const found = results.filter((result) => !!result);
2190
+ const failure = found.find(result => !result.soft);
2191
+ if (failure) {
2192
+ this.sendStatus(parsed, data, 'NO', failure.text, failure.code, description);
2193
+ return false;
2194
+ }
2195
+ found.forEach(result => {
2196
+ this.send({
2197
+ tag: '*',
2198
+ command: 'NO',
2199
+ attributes: [
2200
+ { type: 'SECTION', section: [{ type: 'ATOM', value: result.code }] },
2201
+ { type: 'TEXT', value: result.text }
2202
+ ]
2203
+ }, 'CHECK WARNING', parsed, data);
2204
+ });
2205
+ return true;
2206
+ }
2207
+ /**
2208
+ * Converts a mailbox name from a command to the name used in storage, which is modified UTF-7
2209
+ * (RFC 3501 section 5.1.3). A plugin can replace this per connection, e.g. UTF8=ACCEPT.
2210
+ *
2211
+ * @param {String} name Mailbox name as a binary string
2212
+ * @return {String} Storage name
2213
+ * @throws {Error} BAD error if the name is not valid
2214
+ */
2215
+ importMailboxName(name) {
2216
+ const error = validateMailboxName(name);
2217
+ if (error) {
2218
+ const err = new Error(error);
2219
+ err.imapResponse = 'BAD';
2220
+ throw err;
2221
+ }
2222
+ return name;
2223
+ }
2224
+ /**
2225
+ * Converts a mailbox name from storage to the form sent to the client. Every response that
2226
+ * includes a mailbox name must use this. A plugin can replace this per connection.
2227
+ *
2228
+ * @param {String} path Storage name
2229
+ * @return {String} Mailbox name as a binary string
2230
+ */
2231
+ exportMailboxName(path) {
2232
+ return path;
2233
+ }
2234
+ scheduleCommand(data) {
2235
+ let parsed;
2236
+ const tag = getResponseTag(data);
2237
+ try {
2238
+ // server.parserOptions are the defaults of plugins, connection.parserOptions win
2239
+ parsed = imapHandler.parser(data, Object.assign({ literalPlus: this.server.literalPlus }, this.server.parserOptions, this.parserOptions));
2240
+ }
2241
+ catch (E) {
2242
+ const error = E;
2243
+ this.send({
2244
+ tag: '*',
2245
+ command: 'BAD',
2246
+ attributes: [
2247
+ {
2248
+ type: 'SECTION',
2249
+ section: [
2250
+ {
2251
+ type: 'ATOM',
2252
+ value: 'SYNTAX'
2253
+ }
2254
+ ]
2255
+ },
2256
+ {
2257
+ type: 'TEXT',
2258
+ value: error.message
2259
+ }
2260
+ ]
2261
+ }, 'ERROR MESSAGE', null, data, error);
2262
+ this.send({
2263
+ tag: tag,
2264
+ command: 'BAD',
2265
+ attributes: [
2266
+ {
2267
+ type: 'TEXT',
2268
+ value: 'Error parsing command'
2269
+ }
2270
+ ]
2271
+ }, 'ERROR RESPONSE', null, data, error);
2272
+ return;
2273
+ }
2274
+ if (this.isPipelinedAfterRefusal()) {
2275
+ this.refusePipelined(parsed, data);
2276
+ return;
2277
+ }
2278
+ if (this.server.getCommandHandler(parsed.command)) {
2279
+ if (this.isAmbiguous(parsed)) {
2280
+ this.sendStatus(parsed, data, 'BAD', 'Commands with message sequence numbers must wait for the completion of earlier commands');
2281
+ return;
2282
+ }
2283
+ this._commandQueue.push({
2284
+ parsed: parsed,
2285
+ data: data
2286
+ });
2287
+ this.processQueue();
2288
+ }
2289
+ else if (/^AUTHENTICATE /i.test(parsed.command)) {
2290
+ // an unsupported mechanism is a NO, not a syntax error (RFC 3501 section 6.2.2)
2291
+ this.send({
2292
+ tag: parsed.tag,
2293
+ command: 'NO',
2294
+ attributes: [
2295
+ {
2296
+ type: 'TEXT',
2297
+ value: 'Unsupported authentication mechanism'
2298
+ }
2299
+ ]
2300
+ }, 'UNKNOWN COMMAND', parsed, data);
2301
+ }
2302
+ else {
2303
+ this.send({
2304
+ tag: parsed.tag,
2305
+ command: 'BAD',
2306
+ attributes: [
2307
+ {
2308
+ type: 'TEXT',
2309
+ value: 'Invalid command ' + parsed.command + ''
2310
+ }
2311
+ ]
2312
+ }, 'UNKNOWN COMMAND', parsed, data);
2313
+ }
2314
+ }
2315
+ processQueue(force) {
2316
+ if (!force && this._processing) {
2317
+ return;
2318
+ }
2319
+ if (!this._commandQueue.length) {
2320
+ this._processing = false;
2321
+ return;
2322
+ }
2323
+ this._processing = true;
2324
+ const element = this._commandQueue.shift();
2325
+ const command = element.parsed.command.toUpperCase();
2326
+ this._runningCommand = element;
2327
+ const options = this.server.getCommandOptions(command);
2328
+ let done = false;
2329
+ const next = () => {
2330
+ if (done) {
2331
+ // a handler must release the queue only once
2332
+ return;
2333
+ }
2334
+ done = true;
2335
+ this._runningCommand = null;
2336
+ if (!options.noExpunge) {
2337
+ // commands with sequence numbers that arrive in the same read did not wait for this one
2338
+ this._unsafeCompletedRead = this._readCount;
2339
+ }
2340
+ if (!this._commandQueue.length) {
2341
+ this._processing = false;
2342
+ }
2343
+ else {
2344
+ this.processQueue(true);
2345
+ }
2346
+ };
2347
+ if (options.states && options.states.indexOf(this.state) < 0) {
2348
+ this.sendStatus(element.parsed, element.data, 'BAD', stateError(command, this.state));
2349
+ return next();
2350
+ }
2351
+ if (element.parsed.attributes && options.noArguments) {
2352
+ this.sendStatus(element.parsed, element.data, 'BAD', command + ' does not take any arguments');
2353
+ return next();
2354
+ }
2355
+ if (options.noPipelining && this.hasPendingInput()) {
2356
+ // the layer change is not made. The commands that follow were meant to run in the new layer (under TLS, or
2357
+ // compressed), so none of them runs: they are refused like this command, see refusePipelined
2358
+ this.sendStatus(element.parsed, element.data, 'BAD', 'Commands must not be pipelined after ' + command);
2359
+ this._pipelinedAfter = { command, read: this._readCount };
2360
+ this._commandQueue.splice(0).forEach(queued => this.refusePipelined(queued.parsed, queued.data));
2361
+ return next();
2362
+ }
2363
+ // the parser reads every NIL atom as nil, but in an astring NIL is just a name (e.g. SELECT NIL)
2364
+ restoreNilAtoms(element.parsed, element.data, path => (path.length === 1 && (options.mailboxArguments.includes(path[0]) || options.astringArguments.includes(path[0]))) ||
2365
+ (options.searchCriteria !== false && path[0] >= options.searchCriteria));
2366
+ const nameError = importMailboxArguments(this, element.parsed, options.mailboxArguments);
2367
+ if (nameError) {
2368
+ this.sendStatus(element.parsed, element.data, 'BAD', nameError);
2369
+ return next();
2370
+ }
2371
+ for (const check of this.server.commandChecks) {
2372
+ const refusal = check(this, element.parsed);
2373
+ if (refusal) {
2374
+ this.sendStatus(element.parsed, element.data, refusal.command || 'BAD', refusal.text, refusal.code);
2375
+ return next();
2376
+ }
2377
+ }
2378
+ if (command.substr(0, 4) === 'UID ' && this.hasPendingExpunge()) {
2379
+ // EXPUNGE responses may be sent during UID commands (RFC 3501 section 7.4.1). The expunges of other sessions
2380
+ // are reported first, then the command runs on the current mailbox, where the UIDs of the expunged messages
2381
+ // do not exist and are ignored (RFC 3501 section 6.4.8), so the ghost handling of STORE, COPY and MOVE (RFC 2180
2382
+ // section 4) only applies to their sequence number forms. Not for UID SEARCH with message numbers in the
2383
+ // criteria, processNotifications knows when EXPUNGE must wait
2384
+ this.processNotifications(element.parsed);
2385
+ }
2386
+ try {
2387
+ // changes made while the handler runs are attributed to this session (the `origin` of notifications)
2388
+ this.server.activeConnection = this;
2389
+ this.server.getCommandHandler(element.parsed.command)(this, element.parsed, element.data, next);
2390
+ }
2391
+ catch (E) {
2392
+ const ex = E;
2393
+ const badInput = ex.imapResponse === 'BAD';
2394
+ if (!badInput && this.options.debug) {
2395
+ console.error('Error processing command:', ex, '\n', ex.stack);
2396
+ }
2397
+ this.send({
2398
+ tag: element.parsed.tag,
2399
+ command: badInput ? 'BAD' : 'NO',
2400
+ attributes: [].concat(badInput
2401
+ ? []
2402
+ : {
2403
+ type: 'SECTION',
2404
+ section: [
2405
+ {
2406
+ type: 'ATOM',
2407
+ value: 'SERVERBUG'
2408
+ }
2409
+ ]
2410
+ }, {
2411
+ type: 'TEXT',
2412
+ value: badInput ? ex.message : 'Server error: ' + ex.message
2413
+ })
2414
+ }, badInput ? 'INVALID COMMAND' : 'SERVER ERROR', element.parsed, element.data);
2415
+ // keep the connection usable, otherwise every later command would hang
2416
+ next();
2417
+ }
2418
+ finally {
2419
+ this.server.activeConnection = null;
2420
+ }
2421
+ }
2422
+ /**
2423
+ * Removes messages with \Deleted flag
2424
+ *
2425
+ * @param {Object} mailbox Mailbox to check for
2426
+ * @param {Boolean} [ignoreSelf] If set to true, does not send any notices to itself
2427
+ * @param {Boolean} [ignoreSelf] If set to true, does not send EXISTS notice to itself
2428
+ */
2429
+ expungeDeleted(mailbox, ignoreSelf, ignoreExists) {
2430
+ this.expungeSpecificMessages(mailbox, (message) => {
2431
+ return message.flags.indexOf('\\Deleted') >= 0;
2432
+ }, ignoreSelf, ignoreExists);
2433
+ }
2434
+ /**
2435
+ * Given a set of messages in a mailbox (possibly via getMessageRange), remove
2436
+ * them from the mailbox and generate EXPUNGE notifications.
2437
+ *
2438
+ * @param {Object} mailbox Mailbox to check for
2439
+ * @param {Function|Array} messagesOrFilterFunc An Array of messages in the
2440
+ * folder that should be removed or a filtering function that indicates
2441
+ * messages to be removed by returning true.
2442
+ * @param {Boolean} [ignoreSelf] If set to true, does not send any notices to itself
2443
+ * @param {Boolean} [ignoreSelf] If set to true, does not send EXISTS notice to itself
2444
+ * @param {Boolean} [highestFirst] If set to true, the EXPUNGE responses go from the highest UID to the lowest
2445
+ * (MESSAGELIMIT, RFC 9738 section 3.1), otherwise from the lowest
2446
+ */
2447
+ expungeSpecificMessages(mailbox, messagesOrFilterFunc, ignoreSelf, ignoreExists, highestFirst) {
2448
+ let filterFunc;
2449
+ if (Array.isArray(messagesOrFilterFunc)) {
2450
+ const messageSet = new Set(messagesOrFilterFunc);
2451
+ filterFunc = (message) => messageSet.has(message);
2452
+ }
2453
+ else {
2454
+ filterFunc = messagesOrFilterFunc;
2455
+ }
2456
+ // sequence numbers of the removed messages, each one as it is after the earlier EXPUNGE responses. From the
2457
+ // highest message down, the earlier responses do not change the sequence numbers of the later ones
2458
+ const expunged = [];
2459
+ const kept = [];
2460
+ mailbox.messages.forEach((message, i) => {
2461
+ if (filterFunc(message)) {
2462
+ message.ghost = true;
2463
+ expunged.push({ seq: highestFirst ? i + 1 : kept.length + 1, message });
2464
+ }
2465
+ else {
2466
+ kept.push(message);
2467
+ }
2468
+ });
2469
+ if (!expunged.length) {
2470
+ return;
2471
+ }
2472
+ // old copy is required for those sessions that run FETCH before
2473
+ // displaying the EXPUNGE notice
2474
+ const mailboxCopy = mailbox.messages.slice();
2475
+ // update the list in place, other code might hold a reference to it
2476
+ kept.forEach((message, i) => {
2477
+ mailbox.messages[i] = message;
2478
+ });
2479
+ mailbox.messages.length = kept.length;
2480
+ // lets plugins track the removal (e.g. mod-sequences of CONDSTORE and QRESYNC) before any notification
2481
+ this.server.emit('expunge', mailbox, expunged.map(entry => entry.message), this);
2482
+ (highestFirst ? expunged.slice().reverse() : expunged).forEach(entry => {
2483
+ this.server.notify({
2484
+ tag: '*',
2485
+ attributes: [
2486
+ entry.seq,
2487
+ {
2488
+ type: 'ATOM',
2489
+ value: 'EXPUNGE'
2490
+ }
2491
+ ],
2492
+ // the removed message, for plugins that report it differently (e.g. VANISHED of QRESYNC)
2493
+ message: entry.message
2494
+ }, mailbox, ignoreSelf ? this : false);
2495
+ });
2496
+ this.server.notify({
2497
+ tag: '*',
2498
+ attributes: [
2499
+ mailbox.messages.length,
2500
+ {
2501
+ type: 'ATOM',
2502
+ value: 'EXISTS'
2503
+ }
2504
+ ],
2505
+ // distribute the old mailbox data with the notification
2506
+ mailboxCopy: mailboxCopy
2507
+ }, mailbox, ignoreSelf || ignoreExists ? this : false);
2508
+ }
2509
+ }
2510
+ /**
2511
+ * Formats a mailbox name for a response: an atom when possible, otherwise a string. NIL and names
2512
+ * like \\Foo would not read back as mailbox names, so these are strings as well
2513
+ *
2514
+ * @param {String} name Mailbox name as sent to the client
2515
+ * @return {Object} Response attribute
2516
+ */
2517
+ function mailboxAttribute(name) {
2518
+ return { type: ATOM_REGEX.test(name) && !/^NIL$/i.test(name) ? 'ATOM' : 'STRING', value: name };
2519
+ }
2520
+ /**
2521
+ * Checks the mailbox name arguments of a command and replaces them with the storage names.
2522
+ * Arguments that are not strings are left to the command handler.
2523
+ *
2524
+ * @param {Object} connection IMAP connection
2525
+ * @param {Object} parsed Parsed command
2526
+ * @param {Array} positions Argument positions that hold mailbox names
2527
+ * @return {String|Boolean} Description of the problem, or false if the names are valid
2528
+ */
2529
+ function importMailboxArguments(connection, parsed, positions) {
2530
+ for (const position of positions) {
2531
+ const attr = (parsed.attributes || [])[position];
2532
+ if (attr && ['STRING', 'ATOM', 'LITERAL'].indexOf(attr.type) >= 0) {
2533
+ try {
2534
+ attr.value = connection.importMailboxName(attr.value);
2535
+ }
2536
+ catch (err) {
2537
+ return err.message;
2538
+ }
2539
+ }
2540
+ }
2541
+ return false;
2542
+ }
2543
+ export { TAG_REGEX, IMAPServer, IMAPConnection };