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