emailengine-app 1.14.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 (405) hide show
  1. package/.eslintrc +14 -0
  2. package/.github/CODE_OF_CONDUCT.md +76 -0
  3. package/.github/FUNDING.yml +4 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  6. package/.github/contributing.md +17 -0
  7. package/.ncurc.js +10 -0
  8. package/.prettierrc.js +8 -0
  9. package/Dockerfile +17 -0
  10. package/Gruntfile.js +16 -0
  11. package/LICENSE.txt +661 -0
  12. package/README.md +524 -0
  13. package/bin/emailengine.js +14 -0
  14. package/config/default.toml +39 -0
  15. package/docker-compose.yml +47 -0
  16. package/encrypt.js +179 -0
  17. package/examples/api.md +137 -0
  18. package/examples/auth-server.js +104 -0
  19. package/getswagger.sh +8 -0
  20. package/lib/account.js +562 -0
  21. package/lib/append-list.js +67 -0
  22. package/lib/bounce-detect.js +380 -0
  23. package/lib/connection.js +1753 -0
  24. package/lib/consts.js +22 -0
  25. package/lib/db.js +72 -0
  26. package/lib/encrypt.js +100 -0
  27. package/lib/enum-message-flags.js +6 -0
  28. package/lib/get-raw-email.js +292 -0
  29. package/lib/get-secret.js +83 -0
  30. package/lib/logger.js +35 -0
  31. package/lib/lua/s-list-accounts.lua +51 -0
  32. package/lib/lua/z-expunge.lua +20 -0
  33. package/lib/lua/z-get-by-uid.lua +16 -0
  34. package/lib/lua/z-get-mailbox-id.lua +15 -0
  35. package/lib/lua/z-get-mailbox-path.lua +4 -0
  36. package/lib/lua/z-get.lua +15 -0
  37. package/lib/lua/z-push.lua +14 -0
  38. package/lib/lua/z-set.lua +17 -0
  39. package/lib/mailbox.js +1545 -0
  40. package/lib/message-port-stream.js +79 -0
  41. package/lib/schemas.js +311 -0
  42. package/lib/settings.js +63 -0
  43. package/lib/tools.js +488 -0
  44. package/package.json +79 -0
  45. package/scan.js +111 -0
  46. package/server.js +672 -0
  47. package/static/bootstrap-4.6.0-dist/css/bootstrap-grid.css +3872 -0
  48. package/static/bootstrap-4.6.0-dist/css/bootstrap-grid.css.map +1 -0
  49. package/static/bootstrap-4.6.0-dist/css/bootstrap-grid.min.css +7 -0
  50. package/static/bootstrap-4.6.0-dist/css/bootstrap-grid.min.css.map +1 -0
  51. package/static/bootstrap-4.6.0-dist/css/bootstrap-reboot.css +325 -0
  52. package/static/bootstrap-4.6.0-dist/css/bootstrap-reboot.css.map +1 -0
  53. package/static/bootstrap-4.6.0-dist/css/bootstrap-reboot.min.css +8 -0
  54. package/static/bootstrap-4.6.0-dist/css/bootstrap-reboot.min.css.map +1 -0
  55. package/static/bootstrap-4.6.0-dist/css/bootstrap.css +10298 -0
  56. package/static/bootstrap-4.6.0-dist/css/bootstrap.css.map +1 -0
  57. package/static/bootstrap-4.6.0-dist/css/bootstrap.min.css +7 -0
  58. package/static/bootstrap-4.6.0-dist/css/bootstrap.min.css.map +1 -0
  59. package/static/bootstrap-4.6.0-dist/js/bootstrap.bundle.js +7045 -0
  60. package/static/bootstrap-4.6.0-dist/js/bootstrap.bundle.js.map +1 -0
  61. package/static/bootstrap-4.6.0-dist/js/bootstrap.bundle.min.js +7 -0
  62. package/static/bootstrap-4.6.0-dist/js/bootstrap.bundle.min.js.map +1 -0
  63. package/static/bootstrap-4.6.0-dist/js/bootstrap.js +4432 -0
  64. package/static/bootstrap-4.6.0-dist/js/bootstrap.js.map +1 -0
  65. package/static/bootstrap-4.6.0-dist/js/bootstrap.min.js +7 -0
  66. package/static/bootstrap-4.6.0-dist/js/bootstrap.min.js.map +1 -0
  67. package/static/css/callout.css +63 -0
  68. package/static/css/emailengine.css +33 -0
  69. package/static/favicon/android-chrome-192x192.png +0 -0
  70. package/static/favicon/android-chrome-512x512.png +0 -0
  71. package/static/favicon/apple-touch-icon.png +0 -0
  72. package/static/favicon/favicon-16x16.png +0 -0
  73. package/static/favicon/favicon-32x32.png +0 -0
  74. package/static/favicon/manifest.json +20 -0
  75. package/static/favicon.ico +0 -0
  76. package/static/icons/alarm-fill.svg +3 -0
  77. package/static/icons/alarm.svg +7 -0
  78. package/static/icons/alert-circle-fill.svg +3 -0
  79. package/static/icons/alert-circle.svg +4 -0
  80. package/static/icons/alert-octagon-fill.svg +3 -0
  81. package/static/icons/alert-octagon.svg +5 -0
  82. package/static/icons/alert-square-fill.svg +3 -0
  83. package/static/icons/alert-square.svg +5 -0
  84. package/static/icons/alert-triangle-fill.svg +3 -0
  85. package/static/icons/alert-triangle.svg +5 -0
  86. package/static/icons/archive-fill.svg +3 -0
  87. package/static/icons/archive.svg +4 -0
  88. package/static/icons/arrow-bar-bottom.svg +4 -0
  89. package/static/icons/arrow-bar-left.svg +4 -0
  90. package/static/icons/arrow-bar-right.svg +4 -0
  91. package/static/icons/arrow-bar-up.svg +4 -0
  92. package/static/icons/arrow-clockwise.svg +4 -0
  93. package/static/icons/arrow-counterclockwise.svg +4 -0
  94. package/static/icons/arrow-down-left.svg +4 -0
  95. package/static/icons/arrow-down-right.svg +4 -0
  96. package/static/icons/arrow-down-short.svg +4 -0
  97. package/static/icons/arrow-down.svg +4 -0
  98. package/static/icons/arrow-left-right.svg +5 -0
  99. package/static/icons/arrow-left-short.svg +4 -0
  100. package/static/icons/arrow-left.svg +4 -0
  101. package/static/icons/arrow-repeat.svg +5 -0
  102. package/static/icons/arrow-right-short.svg +4 -0
  103. package/static/icons/arrow-right.svg +4 -0
  104. package/static/icons/arrow-up-down.svg +5 -0
  105. package/static/icons/arrow-up-left.svg +4 -0
  106. package/static/icons/arrow-up-right.svg +4 -0
  107. package/static/icons/arrow-up-short.svg +4 -0
  108. package/static/icons/arrow-up.svg +4 -0
  109. package/static/icons/arrows-angle-contract.svg +5 -0
  110. package/static/icons/arrows-angle-expand.svg +5 -0
  111. package/static/icons/arrows-collapse.svg +5 -0
  112. package/static/icons/arrows-expand.svg +5 -0
  113. package/static/icons/arrows-fullscreen.svg +7 -0
  114. package/static/icons/at.svg +3 -0
  115. package/static/icons/award.svg +4 -0
  116. package/static/icons/backspace-fill.svg +3 -0
  117. package/static/icons/backspace-reverse-fill.svg +3 -0
  118. package/static/icons/backspace-reverse.svg +5 -0
  119. package/static/icons/backspace.svg +5 -0
  120. package/static/icons/bar-chart-fill.svg +5 -0
  121. package/static/icons/bar-chart.svg +3 -0
  122. package/static/icons/battery-charging.svg +5 -0
  123. package/static/icons/battery-full.svg +4 -0
  124. package/static/icons/battery.svg +4 -0
  125. package/static/icons/bell-fill.svg +3 -0
  126. package/static/icons/bell.svg +4 -0
  127. package/static/icons/blockquote-left.svg +4 -0
  128. package/static/icons/blockquote-right.svg +4 -0
  129. package/static/icons/book-half-fill.svg +4 -0
  130. package/static/icons/book.svg +4 -0
  131. package/static/icons/bookmark-fill.svg +3 -0
  132. package/static/icons/bookmark.svg +3 -0
  133. package/static/icons/bootstrap-fill.svg +3 -0
  134. package/static/icons/bootstrap-reboot.svg +3 -0
  135. package/static/icons/bootstrap.svg +4 -0
  136. package/static/icons/box-arrow-bottom-left.svg +4 -0
  137. package/static/icons/box-arrow-bottom-right.svg +4 -0
  138. package/static/icons/box-arrow-down.svg +5 -0
  139. package/static/icons/box-arrow-left.svg +5 -0
  140. package/static/icons/box-arrow-right.svg +5 -0
  141. package/static/icons/box-arrow-up-left.svg +4 -0
  142. package/static/icons/box-arrow-up-right.svg +4 -0
  143. package/static/icons/box-arrow-up.svg +5 -0
  144. package/static/icons/braces.svg +3 -0
  145. package/static/icons/brightness-fill-high.svg +4 -0
  146. package/static/icons/brightness-fill-low.svg +11 -0
  147. package/static/icons/brightness-high.svg +3 -0
  148. package/static/icons/brightness-low.svg +11 -0
  149. package/static/icons/brush.svg +4 -0
  150. package/static/icons/bucket-fill.svg +4 -0
  151. package/static/icons/bucket.svg +4 -0
  152. package/static/icons/building.svg +5 -0
  153. package/static/icons/bullseye.svg +6 -0
  154. package/static/icons/calendar-fill.svg +4 -0
  155. package/static/icons/calendar.svg +4 -0
  156. package/static/icons/camera-video-fill.svg +4 -0
  157. package/static/icons/camera-video.svg +4 -0
  158. package/static/icons/camera.svg +5 -0
  159. package/static/icons/capslock-fill.svg +3 -0
  160. package/static/icons/capslock.svg +3 -0
  161. package/static/icons/chat-fill.svg +3 -0
  162. package/static/icons/chat.svg +3 -0
  163. package/static/icons/check-box.svg +4 -0
  164. package/static/icons/check-circle.svg +4 -0
  165. package/static/icons/check.svg +3 -0
  166. package/static/icons/chevron-compact-down.svg +3 -0
  167. package/static/icons/chevron-compact-left.svg +3 -0
  168. package/static/icons/chevron-compact-right.svg +3 -0
  169. package/static/icons/chevron-compact-up.svg +3 -0
  170. package/static/icons/chevron-down.svg +3 -0
  171. package/static/icons/chevron-left.svg +3 -0
  172. package/static/icons/chevron-right.svg +3 -0
  173. package/static/icons/chevron-up.svg +3 -0
  174. package/static/icons/circle-fill.svg +3 -0
  175. package/static/icons/circle-half.svg +3 -0
  176. package/static/icons/circle-slash.svg +3 -0
  177. package/static/icons/circle.svg +3 -0
  178. package/static/icons/clock-fill.svg +3 -0
  179. package/static/icons/clock.svg +4 -0
  180. package/static/icons/cloud-download.svg +5 -0
  181. package/static/icons/cloud-fill.svg +3 -0
  182. package/static/icons/cloud-upload.svg +5 -0
  183. package/static/icons/cloud.svg +3 -0
  184. package/static/icons/code-slash.svg +3 -0
  185. package/static/icons/code.svg +3 -0
  186. package/static/icons/columns-gutters.svg +3 -0
  187. package/static/icons/columns.svg +4 -0
  188. package/static/icons/command.svg +4 -0
  189. package/static/icons/compass.svg +5 -0
  190. package/static/icons/cone-striped.svg +4 -0
  191. package/static/icons/cone.svg +4 -0
  192. package/static/icons/controller.svg +5 -0
  193. package/static/icons/credit-card.svg +5 -0
  194. package/static/icons/cursor-fill.svg +3 -0
  195. package/static/icons/cursor.svg +3 -0
  196. package/static/icons/dash.svg +3 -0
  197. package/static/icons/diamond-half.svg +3 -0
  198. package/static/icons/diamond.svg +3 -0
  199. package/static/icons/display-fill.svg +5 -0
  200. package/static/icons/display.svg +4 -0
  201. package/static/icons/document-code.svg +4 -0
  202. package/static/icons/document-diff.svg +5 -0
  203. package/static/icons/document-richtext.svg +4 -0
  204. package/static/icons/document-spreadsheet.svg +5 -0
  205. package/static/icons/document-text.svg +4 -0
  206. package/static/icons/document.svg +3 -0
  207. package/static/icons/documents-alt.svg +4 -0
  208. package/static/icons/documents.svg +4 -0
  209. package/static/icons/dot.svg +3 -0
  210. package/static/icons/download.svg +5 -0
  211. package/static/icons/egg-fried.svg +4 -0
  212. package/static/icons/eject-fill.svg +3 -0
  213. package/static/icons/eject.svg +3 -0
  214. package/static/icons/envelope-fill.svg +3 -0
  215. package/static/icons/envelope-open-fill.svg +3 -0
  216. package/static/icons/envelope-open.svg +5 -0
  217. package/static/icons/envelope.svg +4 -0
  218. package/static/icons/eye-fill.svg +4 -0
  219. package/static/icons/eye-slash-fill.svg +5 -0
  220. package/static/icons/eye-slash.svg +6 -0
  221. package/static/icons/eye.svg +4 -0
  222. package/static/icons/filter.svg +3 -0
  223. package/static/icons/flag-fill.svg +4 -0
  224. package/static/icons/flag.svg +4 -0
  225. package/static/icons/folder-fill.svg +3 -0
  226. package/static/icons/folder-symlink-fill.svg +3 -0
  227. package/static/icons/folder-symlink.svg +5 -0
  228. package/static/icons/folder.svg +4 -0
  229. package/static/icons/fonts.svg +3 -0
  230. package/static/icons/forward-fill.svg +3 -0
  231. package/static/icons/forward.svg +3 -0
  232. package/static/icons/gear-fill.svg +3 -0
  233. package/static/icons/gear-wide-connected.svg +4 -0
  234. package/static/icons/gear-wide.svg +3 -0
  235. package/static/icons/gear.svg +4 -0
  236. package/static/icons/geo.svg +5 -0
  237. package/static/icons/graph-down.svg +5 -0
  238. package/static/icons/graph-up.svg +5 -0
  239. package/static/icons/grid-fill.svg +6 -0
  240. package/static/icons/grid.svg +3 -0
  241. package/static/icons/hammer.svg +4 -0
  242. package/static/icons/hash.svg +3 -0
  243. package/static/icons/heart-fill.svg +3 -0
  244. package/static/icons/heart.svg +3 -0
  245. package/static/icons/house-fill.svg +4 -0
  246. package/static/icons/house.svg +4 -0
  247. package/static/icons/image-alt.svg +4 -0
  248. package/static/icons/image-fill.svg +3 -0
  249. package/static/icons/image.svg +5 -0
  250. package/static/icons/images.svg +5 -0
  251. package/static/icons/inbox-fill.svg +4 -0
  252. package/static/icons/inbox.svg +4 -0
  253. package/static/icons/inboxes-fill.svg +4 -0
  254. package/static/icons/inboxes.svg +4 -0
  255. package/static/icons/info-fill.svg +3 -0
  256. package/static/icons/info-square-fill.svg +3 -0
  257. package/static/icons/info-square.svg +5 -0
  258. package/static/icons/info.svg +5 -0
  259. package/static/icons/justify-left.svg +3 -0
  260. package/static/icons/justify-right.svg +3 -0
  261. package/static/icons/justify.svg +3 -0
  262. package/static/icons/kanban-fill.svg +3 -0
  263. package/static/icons/kanban.svg +6 -0
  264. package/static/icons/laptop.svg +4 -0
  265. package/static/icons/layout-sidebar-reverse.svg +4 -0
  266. package/static/icons/layout-sidebar.svg +4 -0
  267. package/static/icons/layout-split.svg +3 -0
  268. package/static/icons/list-check.svg +3 -0
  269. package/static/icons/list-ol.svg +4 -0
  270. package/static/icons/list-task.svg +5 -0
  271. package/static/icons/list-ul.svg +3 -0
  272. package/static/icons/list.svg +3 -0
  273. package/static/icons/lock-fill.svg +4 -0
  274. package/static/icons/lock.svg +3 -0
  275. package/static/icons/map.svg +3 -0
  276. package/static/icons/mic.svg +4 -0
  277. package/static/icons/moon.svg +3 -0
  278. package/static/icons/music-player-fill.svg +4 -0
  279. package/static/icons/music-player.svg +5 -0
  280. package/static/icons/option.svg +3 -0
  281. package/static/icons/outlet.svg +5 -0
  282. package/static/icons/pause-fill.svg +3 -0
  283. package/static/icons/pause.svg +3 -0
  284. package/static/icons/pen.svg +5 -0
  285. package/static/icons/pencil.svg +4 -0
  286. package/static/icons/people-fill.svg +3 -0
  287. package/static/icons/people.svg +3 -0
  288. package/static/icons/person-fill.svg +3 -0
  289. package/static/icons/person.svg +3 -0
  290. package/static/icons/phone-landscape.svg +4 -0
  291. package/static/icons/phone.svg +4 -0
  292. package/static/icons/pie-chart-fill.svg +3 -0
  293. package/static/icons/pie-chart.svg +4 -0
  294. package/static/icons/play-fill.svg +3 -0
  295. package/static/icons/play.svg +3 -0
  296. package/static/icons/plug.svg +4 -0
  297. package/static/icons/plus.svg +4 -0
  298. package/static/icons/power.svg +4 -0
  299. package/static/icons/question-fill.svg +3 -0
  300. package/static/icons/question-square-fill.svg +3 -0
  301. package/static/icons/question-square.svg +4 -0
  302. package/static/icons/question.svg +4 -0
  303. package/static/icons/reply-all-fill.svg +4 -0
  304. package/static/icons/reply-all.svg +4 -0
  305. package/static/icons/reply-fill.svg +3 -0
  306. package/static/icons/reply.svg +3 -0
  307. package/static/icons/screwdriver.svg +3 -0
  308. package/static/icons/search.svg +4 -0
  309. package/static/icons/shield-fill.svg +3 -0
  310. package/static/icons/shield-lock-fill.svg +3 -0
  311. package/static/icons/shield-lock.svg +5 -0
  312. package/static/icons/shield-shaded.svg +4 -0
  313. package/static/icons/shield.svg +3 -0
  314. package/static/icons/shift-fill.svg +3 -0
  315. package/static/icons/shift.svg +3 -0
  316. package/static/icons/skip-backward-fill.svg +4 -0
  317. package/static/icons/skip-backward.svg +3 -0
  318. package/static/icons/skip-end-fill.svg +5 -0
  319. package/static/icons/skip-end.svg +4 -0
  320. package/static/icons/skip-forward-fill.svg +5 -0
  321. package/static/icons/skip-forward.svg +3 -0
  322. package/static/icons/skip-start-fill.svg +4 -0
  323. package/static/icons/skip-start.svg +4 -0
  324. package/static/icons/speaker.svg +4 -0
  325. package/static/icons/square-fill.svg +3 -0
  326. package/static/icons/square-half.svg +3 -0
  327. package/static/icons/square.svg +3 -0
  328. package/static/icons/star-fill.svg +3 -0
  329. package/static/icons/star-half.svg +3 -0
  330. package/static/icons/star.svg +3 -0
  331. package/static/icons/stop-fill.svg +3 -0
  332. package/static/icons/stop.svg +3 -0
  333. package/static/icons/stopwatch-fill.svg +3 -0
  334. package/static/icons/stopwatch.svg +5 -0
  335. package/static/icons/sun.svg +4 -0
  336. package/static/icons/table.svg +7 -0
  337. package/static/icons/tablet-landscape.svg +4 -0
  338. package/static/icons/tablet.svg +4 -0
  339. package/static/icons/tag-fill.svg +3 -0
  340. package/static/icons/tag.svg +4 -0
  341. package/static/icons/terminal-fill.svg +3 -0
  342. package/static/icons/terminal.svg +4 -0
  343. package/static/icons/text-center.svg +3 -0
  344. package/static/icons/text-indent-left.svg +3 -0
  345. package/static/icons/text-indent-right.svg +3 -0
  346. package/static/icons/text-left.svg +3 -0
  347. package/static/icons/text-right.svg +3 -0
  348. package/static/icons/three-dots-vertical.svg +3 -0
  349. package/static/icons/three-dots.svg +3 -0
  350. package/static/icons/toggle-off.svg +3 -0
  351. package/static/icons/toggle-on.svg +3 -0
  352. package/static/icons/toggles.svg +4 -0
  353. package/static/icons/tools.svg +4 -0
  354. package/static/icons/trash-fill.svg +3 -0
  355. package/static/icons/trash.svg +4 -0
  356. package/static/icons/triangle-fill.svg +3 -0
  357. package/static/icons/triangle-half.svg +3 -0
  358. package/static/icons/triangle.svg +3 -0
  359. package/static/icons/trophy.svg +6 -0
  360. package/static/icons/tv-fill.svg +3 -0
  361. package/static/icons/tv.svg +3 -0
  362. package/static/icons/type-bold.svg +3 -0
  363. package/static/icons/type-h1.svg +3 -0
  364. package/static/icons/type-h2.svg +3 -0
  365. package/static/icons/type-h3.svg +3 -0
  366. package/static/icons/type-italic.svg +3 -0
  367. package/static/icons/type-strikethrough.svg +4 -0
  368. package/static/icons/type-underline.svg +4 -0
  369. package/static/icons/type.svg +3 -0
  370. package/static/icons/unlock-fill.svg +4 -0
  371. package/static/icons/unlock.svg +3 -0
  372. package/static/icons/upload.svg +4 -0
  373. package/static/icons/volume-down-fill.svg +4 -0
  374. package/static/icons/volume-down.svg +4 -0
  375. package/static/icons/volume-mute-fill.svg +4 -0
  376. package/static/icons/volume-mute.svg +4 -0
  377. package/static/icons/volume-up-fill.svg +6 -0
  378. package/static/icons/volume-up.svg +6 -0
  379. package/static/icons/wallet.svg +3 -0
  380. package/static/icons/watch.svg +5 -0
  381. package/static/icons/wifi.svg +5 -0
  382. package/static/icons/window.svg +5 -0
  383. package/static/icons/wrench.svg +3 -0
  384. package/static/icons/x-circle-fill.svg +3 -0
  385. package/static/icons/x-circle.svg +5 -0
  386. package/static/icons/x-octagon-fill.svg +3 -0
  387. package/static/icons/x-octagon.svg +4 -0
  388. package/static/icons/x-square-fill.svg +3 -0
  389. package/static/icons/x-square.svg +4 -0
  390. package/static/icons/x.svg +4 -0
  391. package/static/index.html +752 -0
  392. package/static/js/emailengine.js +581 -0
  393. package/static/js/jquery-3.4.1.slim.min.js +2 -0
  394. package/static/js/moment-with-locales-2.24.0.min.js +1 -0
  395. package/static/js/popper.min.js +5 -0
  396. package/static/logo.png +0 -0
  397. package/systemd/emailengine.service +89 -0
  398. package/systemd/nginx-proxy.conf +77 -0
  399. package/views/error.hbs +2 -0
  400. package/workers/api.js +2266 -0
  401. package/workers/arena.js +89 -0
  402. package/workers/imap.js +611 -0
  403. package/workers/smtp.js +278 -0
  404. package/workers/submit.js +214 -0
  405. package/workers/webhooks.js +134 -0
package/lib/mailbox.js ADDED
@@ -0,0 +1,1545 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { serialize, unserialize, compareExisting, normalizePath } = require('./tools');
5
+ const msgpack = require('msgpack5')();
6
+ const he = require('he');
7
+ const libmime = require('libmime');
8
+ const punycode = require('punycode/');
9
+ const EmailReplyParser = require('email-reply-parser');
10
+ const linkify = require('linkifyjs/html');
11
+ const { htmlToText } = require('html-to-text');
12
+ const addressparser = require('nodemailer/lib/addressparser');
13
+ const settings = require('./settings');
14
+ const { bounceDetect } = require('./bounce-detect');
15
+ const appendList = require('./append-list');
16
+
17
+ const {
18
+ MESSAGE_NEW_NOTIFY,
19
+ MAILBOX_DELETED_NOTIFY,
20
+ MESSAGE_DELETED_NOTIFY,
21
+ MESSAGE_UPDATED_NOTIFY,
22
+ MAILBOX_RESET_NOTIFY,
23
+ MAILBOX_NEW_NOTIFY,
24
+ EMAIL_BOUNCE_NOTIFY
25
+ } = require('./consts');
26
+
27
+ // Do not check for flag updates using full sync more often than this value
28
+ const FULL_SYNC_DELAY = 30 * 60 * 1000;
29
+ const MAX_HTML_PARSE_LENGTH = 2 * 1024 * 1024; // do not parse HTML messages larger than 2MB to plaintext
30
+
31
+ class Mailbox {
32
+ constructor(connection, entry) {
33
+ this.status = false;
34
+ this.connection = connection;
35
+ this.path = entry.path;
36
+ this.listingEntry = entry;
37
+ this.syncDisabled = entry.syncDisabled;
38
+
39
+ this.imapClient = this.connection.imapClient;
40
+
41
+ this.logger = this.connection.mainLogger.child({
42
+ sub: 'mailbox',
43
+ path: this.path
44
+ });
45
+
46
+ this.isGmail = connection.isGmail;
47
+ this.isAllMail = this.isGmail && this.listingEntry.specialUse === '\\All';
48
+
49
+ this.selected = false;
50
+
51
+ this.redisKey = BigInt('0x' + crypto.createHash('sha1').update(normalizePath(this.path)).digest('hex')).toString(36);
52
+
53
+ this.runPartialSyncTimer = false;
54
+
55
+ this.synced = false;
56
+ }
57
+
58
+ getMailboxStatus() {
59
+ let mailboxInfo = this.imapClient.mailbox;
60
+
61
+ let status = {
62
+ path: this.path
63
+ };
64
+
65
+ status.highestModseq = mailboxInfo.highestModseq ? mailboxInfo.highestModseq : false;
66
+ status.uidValidity = mailboxInfo.uidValidity ? mailboxInfo.uidValidity : false;
67
+ status.uidNext = mailboxInfo.uidNext ? mailboxInfo.uidNext : false;
68
+ status.messages = mailboxInfo.exists ? mailboxInfo.exists : 0;
69
+
70
+ return status;
71
+ }
72
+
73
+ /**
74
+ * Loads last known mailbox state from Redis
75
+ * @returns {Object} mailbox state
76
+ */
77
+ async getStoredStatus() {
78
+ let data = await this.connection.redis.hgetall(this.getMailboxKey());
79
+ data = data || {};
80
+ return {
81
+ path: data.path || this.path,
82
+ uidValidity: data.uidValidity && !isNaN(data.uidValidity) ? BigInt(data.uidValidity) : false,
83
+ highestModseq: data.highestModseq && !isNaN(data.highestModseq) ? BigInt(data.highestModseq) : false,
84
+ messages: data.messages && !isNaN(data.messages) ? Number(data.messages) : false,
85
+ uidNext: data.uidNext && !isNaN(data.uidNext) ? Number(data.uidNext) : false,
86
+ lastFullSync: data.lastFullSync ? new Date(data.lastFullSync) : false
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Updates known mailbox state in Redis
92
+ * @param {Object} data
93
+ */
94
+ async updateStoredStatus(data) {
95
+ if (!data || typeof data !== 'object') {
96
+ return false;
97
+ }
98
+
99
+ let list = Object.keys(data)
100
+ .map(key => {
101
+ switch (key) {
102
+ case 'path':
103
+ case 'uidValidity':
104
+ case 'highestModseq':
105
+ case 'messages':
106
+ case 'uidNext':
107
+ return [key, data[key].toString()];
108
+
109
+ case 'lastFullSync':
110
+ return [key, data[key].toISOString()];
111
+ }
112
+ })
113
+ .filter(entry => entry);
114
+
115
+ if (!list.length) {
116
+ return;
117
+ }
118
+
119
+ await this.connection.redis.hmset(this.getMailboxKey(), Object.fromEntries(list));
120
+ }
121
+
122
+ /**
123
+ * Sets message entry object. Entries are ordered by `uid` property
124
+ * @param {Object} data
125
+ * @param {Number} Sequence number for the added entry
126
+ */
127
+ async entryListSet(data) {
128
+ if (isNaN(data.uid)) {
129
+ return null;
130
+ }
131
+
132
+ return await this.connection.redis.zSet(this.getMessagesKey(), Number(data.uid), serialize(data));
133
+ }
134
+
135
+ /**
136
+ * Retrieves message entry object for the provided sequence value
137
+ * @param {Number} seq Sequence number
138
+ * @returns {Object|null} Message entry object
139
+ */
140
+ async entryListGet(seq, options) {
141
+ let range = Number(seq);
142
+ options = options || {};
143
+ let command = options.uid ? 'zGetByUidBuffer' : 'zGetBuffer';
144
+ let response = await this.connection.redis[command](this.getMessagesKey(), range);
145
+ if (response) {
146
+ try {
147
+ return {
148
+ uid: Number(response[0]),
149
+ entry: unserialize(response[1]),
150
+ seq: Number(response[2])
151
+ };
152
+ } catch (err) {
153
+ return null;
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
159
+ /**
160
+ * Deletes entry from message list for the provided sequence value
161
+ * @param {Number} seq Sequence number
162
+ * @returns {Object|null} Message entry object that was deleted
163
+ */
164
+ async entryListExpunge(seq) {
165
+ let response = await this.connection.redis.zExpungeBuffer(this.getMessagesKey(), this.getMailboxKey(), seq);
166
+ if (response) {
167
+ try {
168
+ return unserialize(response[1]);
169
+ } catch (err) {
170
+ return null;
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+
176
+ isSelected() {
177
+ return this.selected && this.imapClient.mailbox && normalizePath(this.imapClient.mailbox.path) === normalizePath(this.path);
178
+ }
179
+
180
+ getMessagesKey() {
181
+ return `iam:${this.connection.account}:l:${this.redisKey}`;
182
+ }
183
+
184
+ getMailboxKey() {
185
+ return `iam:${this.connection.account}:h:${this.redisKey}`;
186
+ }
187
+
188
+ getBounceKey() {
189
+ return `iar:b:${this.connection.account}`;
190
+ }
191
+
192
+ startIdle() {
193
+ if (!this.isSelected() || this.imapClient.idling) {
194
+ return;
195
+ }
196
+ this.imapClient.idle().catch(err => {
197
+ this.logger.error({ msg: 'IDLE error', err });
198
+ });
199
+ }
200
+
201
+ // clear mailbox records
202
+ async clear(opts) {
203
+ opts = opts || {};
204
+
205
+ clearTimeout(this.runPartialSyncTimer);
206
+
207
+ await this.connection.redis.del(this.getMailboxKey());
208
+ await this.connection.redis.del(this.getMessagesKey());
209
+
210
+ this.logger.debug({ msg: 'Deleted mailbox', path: this.listingEntry.path });
211
+
212
+ if (!opts.skipNotify) {
213
+ this.connection.notify(this, MAILBOX_DELETED_NOTIFY, {
214
+ path: this.listingEntry.path,
215
+ name: this.listingEntry.name,
216
+ specialUse: this.listingEntry.specialUse || false
217
+ });
218
+ }
219
+ }
220
+
221
+ async sync() {
222
+ if (this.selected) {
223
+ // expect current folder to be already synced
224
+ return false;
225
+ }
226
+
227
+ let status = await this.imapClient.status(this.path, {
228
+ uidNext: true,
229
+ messages: true,
230
+ highestModseq: true,
231
+ uidValidity: true
232
+ });
233
+
234
+ if (!status) {
235
+ // nothing to do here
236
+ return;
237
+ }
238
+
239
+ status.highestModseq = status.highestModseq || false;
240
+
241
+ if (this.syncDisabled) {
242
+ // only update counters
243
+ await this.updateStoredStatus(status);
244
+ return true;
245
+ }
246
+
247
+ let storedStatus = await this.getStoredStatus();
248
+ if (status.uidValidity === storedStatus.uidValidity) {
249
+ if (
250
+ status.uidNext === storedStatus.uidNext &&
251
+ status.messages === storedStatus.messages &&
252
+ storedStatus.lastFullSync > new Date(Date.now() - FULL_SYNC_DELAY)
253
+ ) {
254
+ // no reason to sync
255
+ return true;
256
+ }
257
+
258
+ if ((!status.messages && !storedStatus.messages) || (status.highestModseq && status.highestModseq === storedStatus.highestModseq)) {
259
+ // no reason to sync
260
+ return true;
261
+ }
262
+ }
263
+
264
+ let syncedPromise = new Promise((resolve, reject) => {
265
+ this.synced = resolve;
266
+ this.select(true).catch(err => reject(err));
267
+ });
268
+
269
+ await syncedPromise;
270
+ }
271
+
272
+ async select(skipIdle) {
273
+ let lock = await this.imapClient.getMailboxLock(this.path);
274
+ // have to release the lock immediatelly, otherwise difficult to process 'exists' / 'expunge' events
275
+ lock.release();
276
+
277
+ if (!skipIdle) {
278
+ // do not wait until command finishes before proceeding
279
+ this.startIdle();
280
+ }
281
+ }
282
+
283
+ logEvent(msg, event) {
284
+ const logObj = Object.assign({ msg }, event);
285
+ Object.keys(logObj).forEach(key => {
286
+ if (typeof logObj[key] === 'bigint') {
287
+ logObj[key] = logObj[key].toString();
288
+ }
289
+ if (typeof logObj[key].has === 'function') {
290
+ logObj[key] = Array.from(logObj[key]);
291
+ }
292
+ });
293
+ this.logger.trace(logObj);
294
+ }
295
+
296
+ async onExists(event) {
297
+ this.logEvent('Untagged EXISTS', event);
298
+
299
+ clearTimeout(this.runPartialSyncTimer);
300
+ this.runPartialSyncTimer = setTimeout(() => {
301
+ this.shouldRunPartialSyncAfterExists()
302
+ .then(shouldRun => {
303
+ if (shouldRun) {
304
+ return this.partialSync();
305
+ }
306
+ return false;
307
+ })
308
+ .then(() => this.select())
309
+ .catch(err => this.logger.error({ msg: 'Sync error', err }));
310
+ }, 1000);
311
+ }
312
+
313
+ async onExpunge(event) {
314
+ this.logEvent('Untagged EXPUNGE', event);
315
+
316
+ let deletedEntry = await this.entryListExpunge(event.seq);
317
+ if (deletedEntry) {
318
+ await this.processDeleted(deletedEntry);
319
+ await this.markUpdated();
320
+ }
321
+ }
322
+
323
+ async onFlags(event) {
324
+ this.logEvent('Untagged FETCH', event);
325
+
326
+ let storedMessage = await this.entryListGet(event.seq, { uid: false });
327
+ let changes;
328
+
329
+ // ignore Recent flag
330
+ event.flags.delete('\\Recent');
331
+
332
+ if (!storedMessage) {
333
+ // New! There should not be new messages.
334
+ // What should we do? Currently triggering partial sync.
335
+ return await this.onExists();
336
+ } else if ((changes = compareExisting(storedMessage.entry, event, ['flags']))) {
337
+ let messageData = storedMessage.entry;
338
+ messageData.flags = event.flags;
339
+ let seq = await this.entryListSet(messageData);
340
+ if (seq) {
341
+ await this.processChanges(storedMessage, changes);
342
+ }
343
+ }
344
+ }
345
+
346
+ async shouldRunPartialSyncAfterExists() {
347
+ let storedStatus = await this.getStoredStatus();
348
+ let mailboxStatus = this.getMailboxStatus();
349
+ return mailboxStatus.messages !== storedStatus.messages;
350
+ }
351
+
352
+ async partialSync(storedStatus) {
353
+ storedStatus = storedStatus || (await this.getStoredStatus());
354
+ let mailboxStatus = this.getMailboxStatus();
355
+
356
+ let lock = await this.imapClient.getMailboxLock(this.path);
357
+ try {
358
+ let newMessages = [];
359
+
360
+ let fields = { uid: true, flags: true, modseq: true, emailId: true, labels: true, internalDate: true };
361
+ let range = '1:*';
362
+ let opts = {
363
+ uid: true
364
+ };
365
+
366
+ if (this.imapClient.enabled.has('CONDSTORE') && storedStatus.highestModseq) {
367
+ opts.changedSince = storedStatus.highestModseq;
368
+ } else if (storedStatus.uidNext) {
369
+ range = `${storedStatus.uidNext}:*`;
370
+ }
371
+
372
+ if (mailboxStatus.messages) {
373
+ // only fetch messages if there is some
374
+ for await (let messageData of this.imapClient.fetch(range, fields, opts)) {
375
+ // ignore Recent flag
376
+ messageData.flags.delete('\\Recent');
377
+
378
+ let storedMessage = await this.entryListGet(messageData.uid, { uid: true });
379
+
380
+ let changes;
381
+ if (!storedMessage) {
382
+ // new!
383
+ let seq = await this.entryListSet(messageData);
384
+ if (seq) {
385
+ newMessages.push(messageData);
386
+ }
387
+ } else if ((changes = compareExisting(storedMessage.entry, messageData))) {
388
+ let seq = await this.entryListSet(messageData);
389
+ if (seq) {
390
+ await this.processChanges(messageData, changes);
391
+ }
392
+ }
393
+ }
394
+ }
395
+
396
+ await this.updateStoredStatus(this.getMailboxStatus());
397
+
398
+ let messageFetchOptions = {};
399
+ let notifyText = await settings.get('notifyText');
400
+ if (notifyText) {
401
+ messageFetchOptions.textType = '*';
402
+ let notifyTextSize = await settings.get('notifyTextSize');
403
+ if (notifyTextSize) {
404
+ messageFetchOptions.maxBytes = notifyTextSize;
405
+ }
406
+ }
407
+
408
+ let notifyHeaders = await settings.get('notifyHeaders');
409
+ if (notifyHeaders) {
410
+ messageFetchOptions.headers = notifyHeaders.includes('*') ? true : notifyHeaders.length ? notifyHeaders : false;
411
+ }
412
+
413
+ // have to call after fetch is finished
414
+ for (let messageData of newMessages) {
415
+ if (this.connection.notifyFrom && messageData.internalDate < this.connection.notifyFrom) {
416
+ // skip too old messages
417
+ continue;
418
+ }
419
+ await this.processNew(messageData, messageFetchOptions);
420
+ }
421
+
422
+ if (newMessages.length) {
423
+ await this.markUpdated();
424
+ }
425
+ } finally {
426
+ lock.release();
427
+ }
428
+ }
429
+
430
+ async processDeleted(messageData) {
431
+ this.logger.debug({ msg: 'Deleted', uid: messageData.uid });
432
+
433
+ //FIXME: does not work as there is no messageId property
434
+ /*
435
+ if (messageData.messageId) {
436
+ try {
437
+ let deleted = await appendList.clear(this.connection.redis, this.getBounceKey(), messageData.messageId);
438
+ if (deleted) {
439
+ this.logger.error({
440
+ msg: 'Cleared bounce log for message',
441
+ id: messageData.id,
442
+ uid: messageData.uid,
443
+ messageId: messageData.messageId
444
+ });
445
+ }
446
+ } catch (err) {
447
+ this.logger.error({
448
+ msg: 'Failed to clear bounce log',
449
+ id: messageData.id,
450
+ uid: messageData.uid,
451
+ messageId: messageData.messageId,
452
+ err
453
+ });
454
+ }
455
+ }
456
+ */
457
+
458
+ let packedUid = await this.connection.packUid(this, messageData.uid);
459
+ await this.connection.notify(this, MESSAGE_DELETED_NOTIFY, {
460
+ id: packedUid,
461
+ uid: messageData.uid
462
+ });
463
+ }
464
+
465
+ async processNew(messageData, options) {
466
+ this.logger.debug({ msg: 'New message', uid: messageData.uid, flags: Array.from(messageData.flags) });
467
+
468
+ options.skipLock = true;
469
+ options.headers = 'headers' in options ? options.headers : false;
470
+ options.skipVisible = true;
471
+
472
+ let messageInfo = await this.getMessage(messageData, options);
473
+ if (!messageInfo) {
474
+ this.logger.debug({ msg: 'Not found', uid: messageData.uid });
475
+ return;
476
+ }
477
+
478
+ let date = new Date(messageInfo.date);
479
+ if (this.connection.notifyFrom && date < this.connection.notifyFrom) {
480
+ // skip too old messages
481
+ return;
482
+ }
483
+
484
+ let bounceNotifyInfo;
485
+
486
+ // Check if this could be a bounce
487
+ if (this.mightBeABounce(messageInfo)) {
488
+ // parse for bounce
489
+ try {
490
+ let { content } = await this.imapClient.download(messageInfo.uid, false, {
491
+ uid: true,
492
+ // future feature
493
+ chunkSize: options.chunkSize
494
+ });
495
+ if (content) {
496
+ let bounce = await bounceDetect(content);
497
+
498
+ let stored = 0;
499
+ if (bounce.action && bounce.recipient && bounce.messageId) {
500
+ let storedBounce = {
501
+ i: messageInfo.id,
502
+ r: bounce.recipient,
503
+ t: Date.now(),
504
+ a: bounce.action
505
+ };
506
+
507
+ if (bounce.response && bounce.response.message) {
508
+ storedBounce.m = bounce.response.message;
509
+ }
510
+
511
+ if (bounce.response && bounce.response.status) {
512
+ storedBounce.s = bounce.response.status;
513
+ }
514
+
515
+ // Store bounce info
516
+ stored = await appendList.append(this.connection.redis, this.getBounceKey(), bounce.messageId, storedBounce);
517
+
518
+ bounceNotifyInfo = Object.assign({ bounceMessage: messageInfo.id }, bounce);
519
+ }
520
+
521
+ this.logger.debug({
522
+ msg: 'Detected bounce message',
523
+ id: messageInfo.id,
524
+ uid: messageInfo.uid,
525
+ messageId: messageInfo.messageId,
526
+ bounce,
527
+ stored
528
+ });
529
+ }
530
+ } catch (err) {
531
+ this.logger.error({
532
+ msg: 'Failed to process potential bounce',
533
+ id: messageInfo.id,
534
+ uid: messageInfo.uid,
535
+ messageId: messageInfo.messageId,
536
+ err
537
+ });
538
+ }
539
+ }
540
+
541
+ await this.connection.notify(this, MESSAGE_NEW_NOTIFY, messageInfo);
542
+ if (bounceNotifyInfo) {
543
+ // send bounce notification _after_ bounce email notification
544
+ await this.connection.notify(false, EMAIL_BOUNCE_NOTIFY, bounceNotifyInfo);
545
+ }
546
+ }
547
+
548
+ async getMessageInfo(messageData, extended) {
549
+ if (!messageData) {
550
+ return false;
551
+ }
552
+
553
+ let packedUid = await this.connection.packUid(this, messageData.uid);
554
+ let { attachments, textId, encodedTextSize } = this.getAttachmentList(packedUid, messageData.bodyStructure);
555
+
556
+ let envelope = messageData.envelope || {};
557
+ let date = envelope.date || messageData.internalDate;
558
+
559
+ let result = {
560
+ id: packedUid,
561
+ uid: messageData.uid,
562
+
563
+ emailId: messageData.emailId || undefined,
564
+ threadId: messageData.threadId || undefined,
565
+
566
+ date: (date && date.toISOString()) || undefined,
567
+ flags: (extended && messageData.flags && messageData.flags.size && Array.from(messageData.flags)) || undefined,
568
+
569
+ unseen: messageData.flags && !messageData.flags.has('\\Seen') ? true : undefined,
570
+ flagged: messageData.flags && messageData.flags.has('\\Flagged') ? true : undefined,
571
+ answered: messageData.flags && messageData.flags.has('\\Answered') ? true : undefined,
572
+ draft: messageData.flags && messageData.flags.has('\\Draft') ? true : undefined,
573
+
574
+ size: messageData.size || undefined,
575
+ subject: envelope.subject || undefined,
576
+ from: envelope.from && envelope.from[0] ? envelope.from[0] : undefined,
577
+
578
+ replyTo: extended && envelope.replyTo && envelope.replyTo[0] ? envelope.replyTo[0] : undefined,
579
+ sender: extended && envelope.sender && envelope.sender[0] ? envelope.sender[0] : undefined,
580
+
581
+ to: envelope.to && envelope.to.length ? envelope.to : undefined,
582
+ cc: envelope.cc && envelope.cc.length ? envelope.cc : undefined,
583
+
584
+ bcc: extended && envelope.bcc && envelope.bcc.length ? envelope.bcc : undefined,
585
+
586
+ attachments: attachments && attachments.length ? attachments : undefined,
587
+ messageId: (envelope.messageId && envelope.messageId.toString().trim()) || undefined,
588
+ inReplyTo: envelope.inReplyTo || undefined,
589
+
590
+ labels: messageData.labels && messageData.labels.size ? Array.from(messageData.labels) : undefined,
591
+
592
+ headers: (extended && messageData.headers && libmime.decodeHeaders(messageData.headers.toString().trim())) || undefined,
593
+ text: textId
594
+ ? {
595
+ id: textId,
596
+ encodedSize: encodedTextSize
597
+ }
598
+ : undefined
599
+ };
600
+
601
+ Object.keys(result).forEach(key => {
602
+ if (typeof result[key] === 'undefined') {
603
+ delete result[key];
604
+ }
605
+ });
606
+
607
+ // is there a related bounce as well?
608
+ try {
609
+ if (result.messageId) {
610
+ let bounces = await appendList.list(this.connection.redis, this.getBounceKey(), result.messageId);
611
+ if (bounces && bounces.length) {
612
+ result.bounces = bounces.map(row => {
613
+ let bounce = {
614
+ message: row.i,
615
+ recipient: row.r,
616
+ action: row.a
617
+ };
618
+ if (row.m || row.s) {
619
+ bounce.response = {};
620
+ }
621
+ if (row.m) {
622
+ bounce.response.message = row.m;
623
+ }
624
+ if (row.s) {
625
+ bounce.response.status = row.s;
626
+ }
627
+ bounce.date = new Date(row.t).toISOString();
628
+ return bounce;
629
+ });
630
+ }
631
+ }
632
+ } catch (E) {
633
+ this.logger.error({
634
+ msg: 'Failed to fetch bounces',
635
+ id: messageData.id,
636
+ uid: messageData.uid,
637
+ messageId: messageData.messageId,
638
+ err: E
639
+ });
640
+ }
641
+
642
+ return result;
643
+ }
644
+
645
+ getAttachmentList(packedUid, bodyStructure) {
646
+ let attachments = [];
647
+ let textParts = [[], [], []];
648
+ if (!bodyStructure) {
649
+ return attachments;
650
+ }
651
+
652
+ let idBuf = Buffer.from(packedUid.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
653
+
654
+ let encodedTextSize = {};
655
+
656
+ let walk = (node, isRelated) => {
657
+ if (node.type === 'multipart/related') {
658
+ isRelated = true;
659
+ }
660
+
661
+ if (!/^multipart\//.test(node.type)) {
662
+ if (node.disposition === 'attachment' || !/^text\//.test(node.type)) {
663
+ attachments.push({
664
+ // append body part nr to message id
665
+ id: Buffer.concat([idBuf, Buffer.from(node.part || '1')])
666
+ .toString('base64')
667
+ .replace(/\+/g, '-')
668
+ .replace(/\//g, '_')
669
+ .replace(/[=]+/g, ''),
670
+ contentType: node.type,
671
+ encodedSize: node.size,
672
+ filename: (node.dispositionParameters && node.dispositionParameters.filename) || (node.parameters && node.parameters.name) || false,
673
+ embedded: isRelated,
674
+ inline: node.disposition === 'inline' || (!node.disposition && isRelated),
675
+ contentId: node.id
676
+ });
677
+ } else if ((!node.disposition || node.disposition === 'inline') && /^text\//.test(node.type)) {
678
+ let type = node.type.substr(5);
679
+ if (!encodedTextSize[type]) {
680
+ encodedTextSize[type] = 0;
681
+ }
682
+ encodedTextSize[type] += node.size;
683
+ switch (type) {
684
+ case 'plain':
685
+ textParts[0].push(node.part || '1');
686
+ break;
687
+ case 'html':
688
+ textParts[1].push(node.part || '1');
689
+ break;
690
+ default:
691
+ textParts[2].push(node.part || '1');
692
+ break;
693
+ }
694
+ }
695
+ }
696
+
697
+ if (node.childNodes) {
698
+ node.childNodes.forEach(childNode => walk(childNode, isRelated));
699
+ }
700
+ };
701
+
702
+ walk(bodyStructure, false);
703
+
704
+ return {
705
+ attachments,
706
+ textId: Buffer.concat([idBuf, msgpack.encode(textParts)])
707
+ .toString('base64')
708
+ .replace(/\+/g, '-')
709
+ .replace(/\//g, '_')
710
+ .replace(/[=]+/g, ''),
711
+ encodedTextSize
712
+ };
713
+ }
714
+
715
+ async processChanges(messageData, changes) {
716
+ let packedUid = await this.connection.packUid(this, messageData.uid);
717
+ await this.connection.notify(this, MESSAGE_UPDATED_NOTIFY, {
718
+ id: packedUid,
719
+ uid: messageData.uid,
720
+ changes
721
+ });
722
+ await this.markUpdated();
723
+ }
724
+
725
+ async fullSync() {
726
+ let range = '1:*';
727
+ let fields = { uid: true, flags: true, modseq: true, emailId: true, labels: true, internalDate: true };
728
+ let opts = {};
729
+
730
+ let lock = await this.imapClient.getMailboxLock(this.path);
731
+ try {
732
+ let mailboxStatus = this.getMailboxStatus();
733
+ let newMessages = [];
734
+
735
+ // full sync
736
+ let seqMax = 0;
737
+ let changes;
738
+
739
+ if (mailboxStatus.messages) {
740
+ // only fetch messages if there is some
741
+
742
+ for await (let messageData of this.imapClient.fetch(range, fields, opts)) {
743
+ // ignore Recent flag
744
+ messageData.flags.delete('\\Recent');
745
+
746
+ if (messageData.seq > seqMax) {
747
+ seqMax = messageData.seq;
748
+ }
749
+
750
+ let storedMessage = await this.entryListGet(messageData.uid, { uid: true });
751
+ if (!storedMessage) {
752
+ // new!
753
+ let seq = await this.entryListSet(messageData);
754
+ if (seq) {
755
+ newMessages.push(messageData);
756
+ }
757
+ } else {
758
+ let diff = storedMessage.seq - messageData.seq;
759
+ for (let i = diff - 1; i >= 0; i--) {
760
+ let seq = messageData.seq + i;
761
+ let deletedEntry = await this.entryListExpunge(seq);
762
+ if (deletedEntry) {
763
+ await this.processDeleted(deletedEntry);
764
+ }
765
+ }
766
+
767
+ if ((changes = compareExisting(storedMessage.entry, messageData))) {
768
+ let seq = await this.entryListSet(messageData);
769
+ if (seq) {
770
+ await this.processChanges(messageData, changes);
771
+ }
772
+ }
773
+ }
774
+ }
775
+ }
776
+
777
+ // delete unlisted messages
778
+ let storedMaxSeq = await this.connection.redis.zcard(this.getMessagesKey());
779
+ let diff = storedMaxSeq - seqMax;
780
+ for (let i = diff - 1; i >= 0; i--) {
781
+ let seq = seqMax + i + 1;
782
+ let deletedEntry = await this.entryListExpunge(seq);
783
+ if (deletedEntry) {
784
+ await this.processDeleted(deletedEntry);
785
+ }
786
+ }
787
+
788
+ let status = this.getMailboxStatus();
789
+ status.lastFullSync = new Date();
790
+ await this.updateStoredStatus(status);
791
+
792
+ let messageFetchOptions = {};
793
+
794
+ let notifyText = await settings.get('notifyText');
795
+ if (notifyText) {
796
+ messageFetchOptions.textType = '*';
797
+ let notifyTextSize = await settings.get('notifyTextSize');
798
+ if (notifyTextSize) {
799
+ messageFetchOptions.maxBytes = notifyTextSize;
800
+ }
801
+ }
802
+
803
+ let notifyHeaders = await settings.get('notifyHeaders');
804
+ if (notifyHeaders) {
805
+ messageFetchOptions.headers = notifyHeaders.includes('*') ? true : notifyHeaders.length ? notifyHeaders : false;
806
+ }
807
+
808
+ // have to call after fetch is finished
809
+ for (let messageData of newMessages) {
810
+ if (this.connection.notifyFrom && messageData.internalDate < this.connection.notifyFrom) {
811
+ // skip too old messages
812
+ continue;
813
+ }
814
+
815
+ await this.processNew(messageData, messageFetchOptions);
816
+ }
817
+
818
+ if (newMessages.length) {
819
+ await this.markUpdated();
820
+ }
821
+ } finally {
822
+ lock.release();
823
+ }
824
+ }
825
+
826
+ async onOpen() {
827
+ clearTimeout(this.runPartialSyncTimer);
828
+ this.selected = true;
829
+
830
+ let mailboxStatus = this.getMailboxStatus();
831
+
832
+ try {
833
+ let storedStatus = await this.getStoredStatus();
834
+
835
+ if (storedStatus.uidValidity && storedStatus.uidValidity !== mailboxStatus.uidValidity) {
836
+ // UIDVALIDITY has changed, full sync is required!
837
+ // delete mailbox status
838
+ let result = await this.connection.redis.multi().zcard(this.getMessagesKey()).del(this.getMessagesKey()).del(this.getMailboxKey()).exec();
839
+
840
+ let deletedMessages = (result[0] && Number(result[0][1])) || 0;
841
+ this.logger.info({
842
+ msg: 'UIDVALIDITY change',
843
+ deleted: deletedMessages,
844
+ prevUidValidity: storedStatus.uidValidity && storedStatus.uidValidity.toString(),
845
+ uidValidity: mailboxStatus.uidValidity && mailboxStatus.uidValidity.toString()
846
+ });
847
+
848
+ this.logger.debug({ msg: 'Mailbox reset', path: this.listingEntry.path });
849
+ await this.connection.notify(this, MAILBOX_RESET_NOTIFY, {
850
+ path: this.listingEntry.path,
851
+ name: this.listingEntry.name,
852
+ specialUse: this.listingEntry.specialUse || false,
853
+ uidValidity: mailboxStatus.uidValidity && mailboxStatus.uidValidity.toString(),
854
+ prevUidValidity: storedStatus.uidValidity && storedStatus.uidValidity.toString()
855
+ });
856
+
857
+ // do not advertise messages as new
858
+ this.listingEntry.isNew = true;
859
+
860
+ // generates blank stored status as the Redis key was deleted
861
+ storedStatus = await this.getStoredStatus();
862
+ }
863
+
864
+ if (storedStatus.highestModseq && storedStatus.highestModseq === mailboxStatus.highestModseq) {
865
+ return false;
866
+ }
867
+
868
+ if (storedStatus.messages === 0 && mailboxStatus.messages === 0) {
869
+ return false;
870
+ }
871
+
872
+ if (
873
+ this.imapClient.enabled.has('CONDSTORE') &&
874
+ storedStatus.highestModseq < mailboxStatus.highestModseq &&
875
+ storedStatus.messages <= mailboxStatus.messages &&
876
+ mailboxStatus.uidNext - storedStatus.uidNext === mailboxStatus.messages - storedStatus.messages
877
+ ) {
878
+ // search for flag changes and new messages
879
+ return await this.partialSync(storedStatus);
880
+ }
881
+
882
+ if (
883
+ storedStatus.messages < mailboxStatus.messages &&
884
+ mailboxStatus.uidNext - storedStatus.uidNext === mailboxStatus.messages - storedStatus.messages
885
+ ) {
886
+ // seem to have new messages only
887
+ return await this.partialSync(storedStatus);
888
+ }
889
+
890
+ if (
891
+ storedStatus.messages === mailboxStatus.messages &&
892
+ storedStatus.uidNext === mailboxStatus.uidNext &&
893
+ storedStatus.lastFullSync &&
894
+ storedStatus.lastFullSync >= new Date(Date.now() - FULL_SYNC_DELAY)
895
+ ) {
896
+ // too soon from last full sync, message count seems the same
897
+ return false;
898
+ }
899
+
900
+ // Perform full sync. Only way of getting flag changes from non-CONDSTORE servers
901
+ return await this.fullSync();
902
+ } finally {
903
+ if (this.listingEntry.isNew) {
904
+ // fully synced, so not new anymore
905
+ this.listingEntry.isNew = false;
906
+ this.logger.debug({ msg: 'New mailbox', path: this.listingEntry.path });
907
+ this.connection.notify(this, MAILBOX_NEW_NOTIFY, {
908
+ path: this.listingEntry.path,
909
+ name: this.listingEntry.name,
910
+ specialUse: this.listingEntry.specialUse || false,
911
+ uidValidity: mailboxStatus.uidValidity.toString()
912
+ });
913
+ }
914
+
915
+ if (this.synced) {
916
+ this.synced();
917
+ } else {
918
+ await this.select();
919
+ }
920
+ }
921
+ }
922
+
923
+ async onClose() {
924
+ clearTimeout(this.runPartialSyncTimer);
925
+ this.selected = false;
926
+ }
927
+
928
+ async download(stream) {
929
+ return new Promise((resolve, reject) => {
930
+ let chunks = [];
931
+ let chunklen = 0;
932
+ stream.on('error', err => reject(err));
933
+ stream.on('readable', () => {
934
+ let chunk;
935
+ while ((chunk = stream.read()) !== null) {
936
+ if (typeof chunk === 'string') {
937
+ chunk = Buffer.from(chunk);
938
+ }
939
+ if (!chunk || !Buffer.isBuffer(chunk)) {
940
+ // what's that?
941
+ return;
942
+ }
943
+ chunks.push(chunk);
944
+ chunklen += chunk.length;
945
+ }
946
+ });
947
+ stream.on('end', () => {
948
+ resolve(Buffer.concat(chunks, chunklen));
949
+ });
950
+ });
951
+ }
952
+
953
+ // User methods
954
+ // Call `clearTimeout(this.connection.completedTimer);` after locking mailbox
955
+ // Call this.onTaskCompleted() after selected mailbox is processed and lock is released
956
+
957
+ async getText(message, textParts, options) {
958
+ options = options || {};
959
+ let result = {};
960
+
961
+ let maxBytes = options.maxBytes || Infinity;
962
+ let reqMaxBytes = options.maxBytes && !isNaN(options.maxBytes) ? Number(options.maxBytes) + 4 : maxBytes;
963
+
964
+ let hasMore = false;
965
+
966
+ let lock;
967
+ if (!options.skipLock) {
968
+ lock = await this.imapClient.getMailboxLock(this.path);
969
+ clearTimeout(this.connection.completedTimer);
970
+ }
971
+
972
+ try {
973
+ for (let part of textParts) {
974
+ let { meta, content } = await this.imapClient.download(message.uid, part, {
975
+ uid: true,
976
+ // make sure we request enough bytes so we would have complete utf-8 codepoints
977
+ maxBytes: reqMaxBytes,
978
+ // future feature
979
+ chunkSize: options.chunkSize
980
+ });
981
+
982
+ if (!content) {
983
+ continue;
984
+ }
985
+ let text = await this.download(content);
986
+ text = text.toString().replace(/\r?\n/g, '\n');
987
+
988
+ let typeKey = (meta.contentType && meta.contentType.split('/')[1]) || 'plain';
989
+ if (!result[typeKey]) {
990
+ result[typeKey] = [];
991
+ }
992
+
993
+ let typeSize = result[typeKey].reduce((sum, entry) => sum + entry.length, 0);
994
+ if (typeSize >= maxBytes) {
995
+ hasMore = true;
996
+ continue;
997
+ }
998
+ if (typeSize + text.length > maxBytes) {
999
+ text = text.substr(0, maxBytes - typeSize);
1000
+ hasMore = true;
1001
+ }
1002
+ result[typeKey].push(text);
1003
+ }
1004
+ } finally {
1005
+ if (lock) {
1006
+ lock.release();
1007
+ }
1008
+ }
1009
+
1010
+ Object.keys(result).forEach(key => {
1011
+ result[key] = result[key].join('\n');
1012
+ });
1013
+
1014
+ result.hasMore = hasMore;
1015
+
1016
+ if (!options.skipLock) {
1017
+ this.connection.onTaskCompleted();
1018
+ }
1019
+
1020
+ return result;
1021
+ }
1022
+
1023
+ async getAttachment(message, part, options) {
1024
+ options = options || {};
1025
+ let lock = await this.imapClient.getMailboxLock(this.path);
1026
+ clearTimeout(this.connection.completedTimer);
1027
+
1028
+ let streaming = false;
1029
+ let released = false;
1030
+ try {
1031
+ let { meta, content } = await this.imapClient.download(message.uid, part, {
1032
+ uid: true,
1033
+ maxBytes: options.maxBytes,
1034
+ // future feature
1035
+ chunkSize: options.chunkSize
1036
+ });
1037
+
1038
+ if (!meta) {
1039
+ return false;
1040
+ }
1041
+
1042
+ content.headers = {
1043
+ 'content-type': meta.contentType || 'application/octet-stream',
1044
+ 'content-disposition': 'attachment' + (meta.filename ? `; filename=${he.encode(meta.filename)}` : '')
1045
+ };
1046
+
1047
+ content.contentType = meta.contentType;
1048
+ content.filename = meta.filename;
1049
+ content.disposition = meta.disposition;
1050
+ streaming = true;
1051
+
1052
+ content.once('end', () => {
1053
+ if (!released) {
1054
+ released = true;
1055
+ lock.release();
1056
+ }
1057
+ });
1058
+
1059
+ content.once('error', () => {
1060
+ if (!released) {
1061
+ released = true;
1062
+ lock.release();
1063
+ this.connection.onTaskCompleted();
1064
+ }
1065
+ });
1066
+
1067
+ return content;
1068
+ } finally {
1069
+ if (!streaming) {
1070
+ lock.release();
1071
+ this.connection.onTaskCompleted();
1072
+ }
1073
+ }
1074
+ }
1075
+
1076
+ async getMessage(message, options) {
1077
+ options = options || {};
1078
+ let messageInfo;
1079
+
1080
+ try {
1081
+ let lock;
1082
+ if (!options.skipLock) {
1083
+ lock = await this.imapClient.getMailboxLock(this.path);
1084
+ clearTimeout(this.connection.completedTimer);
1085
+ }
1086
+
1087
+ try {
1088
+ let fields = options.fields || {
1089
+ uid: true,
1090
+ flags: true,
1091
+ size: true,
1092
+ bodyStructure: true,
1093
+ envelope: true,
1094
+ internalDate: true,
1095
+ headers: 'headers' in options ? options.headers : true,
1096
+ emailId: true,
1097
+ threadId: true,
1098
+ labels: true
1099
+ };
1100
+
1101
+ let messageData = await this.imapClient.fetchOne(message.uid, fields, { uid: true });
1102
+ if (!messageData) {
1103
+ return false;
1104
+ }
1105
+ messageInfo = await this.getMessageInfo(messageData, true);
1106
+ } finally {
1107
+ if (lock) {
1108
+ lock.release();
1109
+ }
1110
+ }
1111
+
1112
+ if (!messageInfo) {
1113
+ return false;
1114
+ }
1115
+
1116
+ // merge decoded text content with message data (if requested)
1117
+ if (options.textType && messageInfo.text && messageInfo.text.id) {
1118
+ let { textParts } = await this.connection.getMessageTextPaths(messageInfo.text.id);
1119
+ if (textParts && textParts.length) {
1120
+ switch (options.textType) {
1121
+ case 'plain':
1122
+ textParts = textParts[0];
1123
+ break;
1124
+ case 'html':
1125
+ textParts = textParts[1];
1126
+ break;
1127
+ default:
1128
+ textParts = textParts.flatMap(entry => entry);
1129
+ break;
1130
+ }
1131
+
1132
+ if (textParts && textParts.length) {
1133
+ let textContent = await this.getText(message, textParts, options);
1134
+ if (options.textType && options.textType !== '*') {
1135
+ textContent = {
1136
+ [options.textType]: textContent[options.textType] || '',
1137
+ hasMore: textContent.hasMore
1138
+ };
1139
+ }
1140
+ messageInfo.text = Object.assign(messageInfo.text, textContent);
1141
+
1142
+ // Parse visible reply text as well if possible
1143
+ if (
1144
+ !options.skipVisible &&
1145
+ !['plain', 'html'].includes(options.textType) &&
1146
+ (messageInfo.inReplyTo || (messageInfo.headers && messageInfo.headers.references))
1147
+ ) {
1148
+ // might be a reply and not a specific text type was asked
1149
+ try {
1150
+ let plaintext = messageInfo.text.plain || '';
1151
+ if (!plaintext && messageInfo.text.html && messageInfo.text.html.length < MAX_HTML_PARSE_LENGTH) {
1152
+ plaintext = htmlToText(messageInfo.text.html);
1153
+ }
1154
+
1155
+ plaintext = plaintext.trim();
1156
+ if (plaintext) {
1157
+ const email = new EmailReplyParser().read(plaintext);
1158
+ messageInfo.text.visible = linkify(textToHtml(email.getVisibleText()), {
1159
+ defaultProtocol: 'https'
1160
+ });
1161
+ }
1162
+ } catch (err) {
1163
+ this.logger.error({ msg: 'Reply parsing error', err });
1164
+ }
1165
+ }
1166
+ }
1167
+ }
1168
+ }
1169
+ return messageInfo;
1170
+ } finally {
1171
+ if (!options.skipLock) {
1172
+ this.connection.onTaskCompleted();
1173
+ }
1174
+ }
1175
+ }
1176
+
1177
+ async updateMessage(message, updates) {
1178
+ updates = updates || {};
1179
+ let lock = await this.imapClient.getMailboxLock(this.path);
1180
+ clearTimeout(this.connection.completedTimer);
1181
+
1182
+ try {
1183
+ let result = {};
1184
+
1185
+ if (updates.flags) {
1186
+ if (updates.flags.set) {
1187
+ // If set exists the ignore add/delete calls
1188
+ let value = await this.imapClient.messageFlagsSet(message.uid, updates.flags.set, { uid: true });
1189
+ result.flags = {
1190
+ set: value
1191
+ };
1192
+ } else {
1193
+ if (updates.flags.add && updates.flags.add.length) {
1194
+ let value = await this.imapClient.messageFlagsAdd(message.uid, updates.flags.add, { uid: true });
1195
+ if (!result.flags) {
1196
+ result.flags = {};
1197
+ }
1198
+ result.flags.add = value;
1199
+ }
1200
+
1201
+ if (updates.flags.delete && updates.flags.delete.length) {
1202
+ let value = await this.imapClient.messageFlagsRemove(message.uid, updates.flags.delete, { uid: true });
1203
+ if (!result.flags) {
1204
+ result.flags = {};
1205
+ }
1206
+ result.flags.delete = value;
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ if (updates.labels && this.isGmail) {
1212
+ if (updates.labels.set) {
1213
+ // If set exists the ignore add/delete calls
1214
+ let value = await this.imapClient.messageFlagsSet(message.uid, updates.labels.set, { uid: true, useLabels: true });
1215
+ result.labels = {
1216
+ set: value
1217
+ };
1218
+ } else {
1219
+ if (updates.labels.add && updates.labels.add.length) {
1220
+ let value = await this.imapClient.messageFlagsAdd(message.uid, updates.labels.add, { uid: true, useLabels: true });
1221
+ if (!result.labels) {
1222
+ result.labels = {};
1223
+ }
1224
+ result.labels.add = value;
1225
+ }
1226
+
1227
+ if (updates.labels.delete && updates.labels.delete.length) {
1228
+ let value = await this.imapClient.messageFlagsRemove(message.uid, updates.labels.delete, { uid: true, useLabels: true });
1229
+ if (!result.labels) {
1230
+ result.labels = {};
1231
+ }
1232
+ result.labels.delete = value;
1233
+ }
1234
+ }
1235
+ }
1236
+
1237
+ return result;
1238
+ } finally {
1239
+ lock.release();
1240
+ this.connection.onTaskCompleted();
1241
+ }
1242
+ }
1243
+
1244
+ async moveMessage(message, target) {
1245
+ target = target || {};
1246
+
1247
+ let lock = await this.imapClient.getMailboxLock(this.path);
1248
+ clearTimeout(this.connection.completedTimer);
1249
+
1250
+ try {
1251
+ let result = {};
1252
+
1253
+ if (target.path) {
1254
+ // If set exists the ignore add/delete calls
1255
+ let value = await this.imapClient.messageMove(message.uid, target.path, { uid: true });
1256
+ result.path = target.path;
1257
+ if (value && value.uidMap && value.uidMap.has(message.uid)) {
1258
+ let uid = value.uidMap.get(message.uid);
1259
+ let packed = await this.connection.packUid(target.path, uid);
1260
+ result.id = packed;
1261
+ result.uid = uid;
1262
+ }
1263
+ }
1264
+
1265
+ return result;
1266
+ } finally {
1267
+ lock.release();
1268
+ this.connection.onTaskCompleted();
1269
+ }
1270
+ }
1271
+
1272
+ async deleteMessage(message) {
1273
+ let lock = await this.imapClient.getMailboxLock(this.path);
1274
+ clearTimeout(this.connection.completedTimer);
1275
+
1276
+ try {
1277
+ let result = {};
1278
+
1279
+ if (this.listingEntry.specialUse === '\\Trash') {
1280
+ // delete
1281
+ result.deleted = await this.imapClient.messageDelete(message.uid, { uid: true });
1282
+ } else {
1283
+ // move to trash
1284
+ // find Trash folder path
1285
+ let trashMailbox = await this.connection.getSpecialUseMailbox('\\Trash');
1286
+ if (!trashMailbox || normalizePath(trashMailbox.path) === normalizePath(this.path)) {
1287
+ // no Trash found or already in trash
1288
+ result.deleted = await this.imapClient.messageDelete(message.uid, { uid: true });
1289
+ } else {
1290
+ // we have a destionation, so can move message to there
1291
+ let moved = await await this.imapClient.messageMove(message.uid, trashMailbox.path, { uid: true });
1292
+ if (moved) {
1293
+ result.moved = {
1294
+ destination: moved.destination
1295
+ };
1296
+ if (moved && moved.uidMap && moved.uidMap.has(message.uid)) {
1297
+ result.moved.message = await this.connection.packUid(trashMailbox.path, moved.uidMap.get(message.uid));
1298
+ }
1299
+ }
1300
+ }
1301
+ }
1302
+ return result;
1303
+ } finally {
1304
+ lock.release();
1305
+ this.connection.onTaskCompleted();
1306
+ }
1307
+ }
1308
+
1309
+ async listMessages(options) {
1310
+ options = options || {};
1311
+ let page = Number(options.page) || 0;
1312
+ let pageSize = Number(options.pageSize) || 20;
1313
+
1314
+ let lock = await this.imapClient.getMailboxLock(this.path);
1315
+ clearTimeout(this.connection.completedTimer);
1316
+
1317
+ try {
1318
+ let mailboxStatus = this.getMailboxStatus();
1319
+
1320
+ let messageCount = mailboxStatus.messages;
1321
+ let uidList;
1322
+ let opts = {};
1323
+
1324
+ if (options.search) {
1325
+ uidList = await this.imapClient.search(options.search, { uid: true });
1326
+ uidList = !uidList ? [] : uidList.sort((a, b) => b - a); // newer first
1327
+ messageCount = uidList.length;
1328
+ }
1329
+
1330
+ let pages = Math.ceil(messageCount / pageSize) || 1;
1331
+
1332
+ if (page < 0) {
1333
+ page = 0;
1334
+ }
1335
+
1336
+ if (page >= pages) {
1337
+ page = pages - 1;
1338
+ }
1339
+
1340
+ let messages = [];
1341
+ let seqMax, seqMin, range;
1342
+
1343
+ if (!messageCount) {
1344
+ return {
1345
+ page,
1346
+ pages,
1347
+ messages
1348
+ };
1349
+ }
1350
+
1351
+ if (options.search && uidList) {
1352
+ let start = page * pageSize;
1353
+ let uidRange = uidList.slice(start, start + pageSize).reverse();
1354
+ range = uidRange.join(',');
1355
+ opts.uid = true;
1356
+ } else {
1357
+ seqMax = messageCount - page * pageSize;
1358
+ seqMin = seqMax - pageSize + 1;
1359
+
1360
+ if (seqMax >= messageCount) {
1361
+ seqMax = '*';
1362
+ }
1363
+
1364
+ if (seqMin < 1) {
1365
+ seqMin = 1;
1366
+ }
1367
+
1368
+ range = seqMin === seqMax ? `${seqMin}` : `${seqMin}:${seqMax}`;
1369
+ }
1370
+
1371
+ let fields = {
1372
+ uid: true,
1373
+ flags: true,
1374
+ size: true,
1375
+ bodyStructure: true,
1376
+ envelope: true,
1377
+ internalDate: true,
1378
+ emailId: true,
1379
+ threadId: true,
1380
+ labels: true
1381
+ };
1382
+
1383
+ for await (let messageData of this.imapClient.fetch(range, fields, opts)) {
1384
+ let messageInfo = await this.getMessageInfo(messageData);
1385
+ messages.push(messageInfo);
1386
+ }
1387
+
1388
+ return {
1389
+ total: messageCount,
1390
+ page,
1391
+ pages,
1392
+ // list newer first
1393
+ messages: messages.reverse()
1394
+ };
1395
+ } finally {
1396
+ lock.release();
1397
+ this.connection.onTaskCompleted();
1398
+ }
1399
+ }
1400
+
1401
+ async buildContacts() {
1402
+ let lock = await this.imapClient.getMailboxLock(this.path);
1403
+ clearTimeout(this.connection.completedTimer);
1404
+
1405
+ let list = [];
1406
+
1407
+ try {
1408
+ let range = '1:*';
1409
+
1410
+ let fields = {
1411
+ uid: true,
1412
+ envelope: true,
1413
+ headers: ['from', 'to', 'cc']
1414
+ };
1415
+
1416
+ for await (let messageData of this.imapClient.fetch(range, fields)) {
1417
+ let addresses = parseAddressHeaders(messageData.headers);
1418
+ if (addresses && addresses.length) {
1419
+ list = list.concat(addresses);
1420
+ }
1421
+ }
1422
+
1423
+ return list;
1424
+ } finally {
1425
+ lock.release();
1426
+ this.connection.onTaskCompleted();
1427
+ }
1428
+ }
1429
+
1430
+ async markUpdated() {
1431
+ try {
1432
+ await this.connection.redis.hset(this.connection.getAccountKey(), 'sync', new Date().toISOString());
1433
+ } catch (err) {
1434
+ this.logger.error({ msg: 'Redis error', err });
1435
+ }
1436
+ }
1437
+
1438
+ mightBeABounce(messageInfo) {
1439
+ if (this.path !== 'INBOX' && !(this.isAllMail && messageInfo.labels && messageInfo.labels.includes('\\Inbox'))) {
1440
+ return false;
1441
+ }
1442
+
1443
+ let name = (messageInfo.from && messageInfo.from.name) || '';
1444
+ let address = (messageInfo.from && messageInfo.from.address) || '';
1445
+
1446
+ if (/Mail Delivery System|Mail Delivery Subsystem|Internet Mail Delivery/i.test(name)) {
1447
+ return true;
1448
+ }
1449
+
1450
+ if (/mailer-daemon@|postmaster@/i.test(address)) {
1451
+ return true;
1452
+ }
1453
+
1454
+ return false;
1455
+ }
1456
+ }
1457
+
1458
+ function textToHtml(str) {
1459
+ let encoded = he
1460
+ // encode special chars
1461
+ .encode(str, {
1462
+ useNamedReferences: true
1463
+ });
1464
+ let text = `<p>${
1465
+ encoded
1466
+ .replace(/\r?\n/g, '\n')
1467
+ .trim() // normalize line endings
1468
+ .replace(/[ \t]+$/gm, '')
1469
+ .trim() // trim empty line endings
1470
+ .replace(/\n\n+/g, '</p><p>')
1471
+ .trim() // insert <p> to multiple linebreaks
1472
+ .replace(/\n/g, '<br/>') // insert <br> to single linebreaks
1473
+ }</p>`;
1474
+
1475
+ return text;
1476
+ }
1477
+
1478
+ function parseAddressHeaders(headers) {
1479
+ let decodedHeaders = headers && libmime.decodeHeaders(headers.toString().trim());
1480
+ if (!decodedHeaders) {
1481
+ return [];
1482
+ }
1483
+
1484
+ let result = [];
1485
+
1486
+ Object.keys(decodedHeaders).forEach(key => {
1487
+ let value = decodedHeaders[key];
1488
+ switch (key) {
1489
+ case 'from':
1490
+ case 'to':
1491
+ case 'cc':
1492
+ case 'bcc':
1493
+ case 'sender':
1494
+ case 'reply-to':
1495
+ case 'delivered-to':
1496
+ case 'return-path':
1497
+ value = addressparser(value);
1498
+ decodeAddresses(value);
1499
+ break;
1500
+ }
1501
+
1502
+ value.forEach(entry => {
1503
+ result.push(Object.assign({ type: key }, entry));
1504
+ });
1505
+ });
1506
+
1507
+ return result;
1508
+ }
1509
+
1510
+ function decodeAddresses(addresses) {
1511
+ for (let i = 0; i < addresses.length; i++) {
1512
+ let address = addresses[i];
1513
+ address.name = (address.name || '').toString().trim();
1514
+
1515
+ if (!address.address && /^(=\?([^?]+)\?[Bb]\?[^?]*\?=)(\s*=\?([^?]+)\?[Bb]\?[^?]*\?=)*$/.test(address.name)) {
1516
+ let parsed = addressparser(libmime.decodeWords(address.name));
1517
+ if (parsed.length) {
1518
+ parsed.forEach(entry => addresses.push(entry));
1519
+ }
1520
+
1521
+ // remove current element
1522
+ addresses.splice(i, 1);
1523
+ i--;
1524
+ continue;
1525
+ }
1526
+
1527
+ if (address.name) {
1528
+ try {
1529
+ address.name = libmime.decodeWords(address.name);
1530
+ } catch (E) {
1531
+ //ignore, keep as is
1532
+ }
1533
+ }
1534
+ if (/@xn--/.test(address.address)) {
1535
+ address.address =
1536
+ address.address.substr(0, address.address.lastIndexOf('@') + 1) +
1537
+ punycode.toUnicode(address.address.substr(address.address.lastIndexOf('@') + 1));
1538
+ }
1539
+ if (address.group) {
1540
+ decodeAddresses(address.group);
1541
+ }
1542
+ }
1543
+ }
1544
+
1545
+ module.exports.Mailbox = Mailbox;