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