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
@@ -0,0 +1,1753 @@
1
+ 'use strict';
2
+
3
+ const { parentPort } = require('worker_threads');
4
+ const { ImapFlow } = require('imapflow');
5
+ const { Mailbox } = require('./mailbox');
6
+ const logger = require('./logger');
7
+ const crypto = require('crypto');
8
+ const packageData = require('../package.json');
9
+ const { backOff } = require('exponential-backoff');
10
+ const msgpack = require('msgpack5')();
11
+ const nodemailer = require('nodemailer');
12
+ const MailComposer = require('nodemailer/lib/mail-composer');
13
+ const util = require('util');
14
+ const settings = require('./settings');
15
+ const { getRawEmail, removeBcc } = require('./get-raw-email');
16
+
17
+ const { normalizePath, resolveCredentials } = require('./tools');
18
+
19
+ const RESYNC_DELAY = 15 * 60;
20
+ const LIST_REFRESH_DELAY = 30 * 60 * 1000;
21
+ const ENSURE_MAIN_TTL = 5 * 1000;
22
+
23
+ const MAX_BACKOFF_DELAY = 10 * 60 * 1000; // 10 min
24
+
25
+ const { AUTH_ERROR_NOTIFY, AUTH_SUCCESS_NOTIFY, CONNECT_ERROR_NOTIFY, EMAIL_SENT_NOTIFY } = require('./consts');
26
+
27
+ async function metrics(logger, key, method, ...args) {
28
+ try {
29
+ parentPort.postMessage({
30
+ cmd: 'metrics',
31
+ key,
32
+ method,
33
+ args
34
+ });
35
+ } catch (err) {
36
+ logger.error({ msg: 'Failed to post metrics to parent', err });
37
+ }
38
+ }
39
+
40
+ class Connection {
41
+ constructor(options) {
42
+ this.options = options || {};
43
+
44
+ this.account = this.options.account || '';
45
+ this.accountObject = this.options.accountObject;
46
+ this.accountLogger = this.options.accountLogger;
47
+
48
+ this.emitLogs = this.options.accountLogger.enabled;
49
+
50
+ this.cid = this.getRandomId();
51
+
52
+ this.closing = false;
53
+ this.closed = false;
54
+
55
+ this.logger = this.getLogger();
56
+
57
+ this.imapConfig = {
58
+ // Set emitLogs to true if you want to get all the log entries as objects from the IMAP module
59
+ logger: this.mainLogger.child({
60
+ sub: 'imap-connection'
61
+ }),
62
+ clientInfo: {
63
+ name: packageData.name,
64
+ version: packageData.version,
65
+ vendor: (packageData.author && packageData.author.name) || packageData.author,
66
+ 'support-url': (packageData.bugs && packageData.bugs.url) || packageData.bugs
67
+ },
68
+ emitLogs: this.emitLogs
69
+ };
70
+
71
+ this.mailboxes = new Map();
72
+ this.untaggedExistsTimer = false;
73
+ this.redis = options.redis;
74
+ this.notifyQueue = options.notifyQueue;
75
+ this.submitQueue = options.submitQueue;
76
+
77
+ this.refreshListingTimer = false;
78
+ this.resyncTimer = false;
79
+
80
+ this.completedTimer = false;
81
+
82
+ this.pathCache = new Map();
83
+ this.idCache = new Map();
84
+
85
+ this.defaultDelimiter = '/';
86
+
87
+ this.localAddresses = [].concat(this.options.localAddress || []).concat(this.options.localAddresses || []);
88
+
89
+ this.state = 'connecting';
90
+ }
91
+
92
+ getAccountKey() {
93
+ return `iad:${this.account}`;
94
+ }
95
+
96
+ getMailboxListKey() {
97
+ return `ial:${this.account}`;
98
+ }
99
+
100
+ getMailboxHashKey() {
101
+ return `iah:${this.account}`;
102
+ }
103
+
104
+ getLogKey() {
105
+ // this format ensures that the key is deleted when user is removed
106
+ return `iam:${this.account}:g`;
107
+ }
108
+
109
+ getLoggedAccountsKey() {
110
+ return `iaz:logged`;
111
+ }
112
+
113
+ onTaskCompleted() {
114
+ // check if we need to re-select main mailbox
115
+ this.completedTimer = setTimeout(() => {
116
+ clearTimeout(this.completedTimer);
117
+ this.ensureMainMailbox().catch(err => this.logger.error(err));
118
+ }, ENSURE_MAIN_TTL);
119
+ }
120
+
121
+ async ensureMainMailbox() {
122
+ let mainPath = this.main ? this.main.path : 'INBOX';
123
+ if (this.mailbox && normalizePath(this.mailbox.path) === normalizePath(mainPath)) {
124
+ // already selected
125
+ return;
126
+ }
127
+
128
+ // start waiting for changes
129
+ await this.select(mainPath);
130
+ }
131
+
132
+ async packUid(mailbox, uid) {
133
+ if (isNaN(uid)) {
134
+ return false;
135
+ }
136
+
137
+ if (typeof uid !== 'number') {
138
+ uid = Number(uid);
139
+ }
140
+
141
+ if (typeof mailbox === 'string') {
142
+ if (this.mailboxes.has(normalizePath(mailbox))) {
143
+ mailbox = this.mailboxes.get(normalizePath(mailbox));
144
+ } else {
145
+ return false;
146
+ }
147
+ }
148
+
149
+ const storedStatus = await mailbox.getStoredStatus();
150
+ if (!storedStatus.uidValidity || !storedStatus.path) {
151
+ return false;
152
+ }
153
+
154
+ let uidValBuf = Buffer.alloc(8);
155
+ uidValBuf.writeBigUInt64BE(storedStatus.uidValidity, 0);
156
+ let mailboxBuf = Buffer.concat([uidValBuf, Buffer.from(storedStatus.path)]);
157
+
158
+ let mailboxId;
159
+ if (this.pathCache.has(mailboxBuf.toString('hex'))) {
160
+ mailboxId = this.pathCache.get(mailboxBuf.toString('hex'));
161
+ } else {
162
+ mailboxId = await this.redis.zGetMailboxId(this.getAccountKey(), this.getMailboxHashKey(), mailboxBuf);
163
+ if (isNaN(mailboxId) || typeof mailboxId !== 'number') {
164
+ return false;
165
+ }
166
+
167
+ this.pathCache.set(mailboxBuf.toString('hex'), mailboxId);
168
+ this.idCache.set(mailboxId, mailboxBuf);
169
+ }
170
+
171
+ let uidBuf = Buffer.alloc(4 + 4);
172
+ uidBuf.writeUInt32BE(mailboxId, 0);
173
+ uidBuf.writeUInt32BE(uid, 4);
174
+
175
+ let res = uidBuf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]+/g, '');
176
+
177
+ return res;
178
+ }
179
+
180
+ async unpackUid(id) {
181
+ const packed = Buffer.isBuffer(id) ? id : Buffer.from(id.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
182
+
183
+ let mailboxId = packed.readUInt32BE(0);
184
+ let uid = packed.readUInt32BE(4);
185
+
186
+ let mailboxBuf;
187
+ if (this.idCache.has(mailboxId)) {
188
+ mailboxBuf = this.idCache.get(mailboxId);
189
+ } else {
190
+ mailboxBuf = await this.redis.zGetMailboxPathBuffer(this.getMailboxHashKey(), mailboxId);
191
+ if (!mailboxBuf) {
192
+ return false;
193
+ }
194
+
195
+ this.pathCache.set(mailboxBuf.toString('hex'), mailboxId);
196
+ this.idCache.set(mailboxId, mailboxBuf);
197
+ }
198
+
199
+ if (!mailboxBuf) {
200
+ return false;
201
+ }
202
+
203
+ let path = mailboxBuf.slice(8).toString();
204
+ return {
205
+ path,
206
+ uidValidity: mailboxBuf.readBigUInt64BE(0).toString(),
207
+ uid
208
+ };
209
+ }
210
+
211
+ async getMessageTextPaths(textId) {
212
+ let buf = Buffer.from(textId.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
213
+ let id = buf.slice(0, 8);
214
+ let textParts = msgpack.decode(buf.slice(8));
215
+
216
+ let message = await this.unpackUid(id);
217
+ if (!message) {
218
+ return { message: false };
219
+ }
220
+
221
+ return { message, textParts };
222
+ }
223
+
224
+ async clearMailboxEntry(entry) {
225
+ this.checkIMAPConnection();
226
+
227
+ if (!entry || !entry.path) {
228
+ return; // ?
229
+ }
230
+
231
+ let mailbox;
232
+ if (!this.mailboxes.has(normalizePath(entry.path))) {
233
+ mailbox = new Mailbox(this, entry);
234
+ } else {
235
+ mailbox = this.mailboxes.get(normalizePath(entry.path));
236
+ }
237
+
238
+ await mailbox.clear();
239
+ mailbox = false;
240
+ }
241
+
242
+ async getCurrentListing() {
243
+ this.checkIMAPConnection();
244
+
245
+ let listing = await this.imapClient.list();
246
+
247
+ let inboxData = (listing || []).find(entry => /^INBOX$/i.test(entry.path));
248
+ if (inboxData && inboxData.delimiter) {
249
+ this.defaultDelimiter = inboxData.delimiter;
250
+ }
251
+
252
+ // ignore non-selectable folders
253
+ listing = listing.filter(mailbox => !mailbox.flags.has('\\Noselect'));
254
+
255
+ let hasChanges = false;
256
+
257
+ // compare listing for new / deleted / renamed folders
258
+ let storedListing = await this.redis.hgetallBuffer(this.getMailboxListKey());
259
+ storedListing = Object.keys(storedListing || {})
260
+ .map(path => {
261
+ try {
262
+ return msgpack.decode(storedListing[path]);
263
+ } catch (err) {
264
+ // should not happen
265
+ }
266
+ return false;
267
+ })
268
+ .filter(entry => entry);
269
+
270
+ // compare listings
271
+ for (let mailbox of listing) {
272
+ let existingMailbox = storedListing.find(entry => normalizePath(entry.path) === normalizePath(mailbox.path));
273
+ if (!existingMailbox) {
274
+ // found new!
275
+ mailbox.isNew = true;
276
+ hasChanges = true;
277
+ } else if (existingMailbox.delimiter !== mailbox.delimiter) {
278
+ hasChanges = true;
279
+ }
280
+ }
281
+
282
+ for (let entry of storedListing) {
283
+ if (!listing.some(mailbox => normalizePath(entry.path) === normalizePath(mailbox.path))) {
284
+ // found deleted!
285
+ await this.clearMailboxEntry(entry);
286
+ hasChanges = true;
287
+ }
288
+ }
289
+
290
+ // on changes store updated listing
291
+ if (hasChanges) {
292
+ // store
293
+ let listingObject = {};
294
+ listing.forEach(entry => {
295
+ let mailbox = {};
296
+ Object.keys(entry).forEach(key => {
297
+ if (['path', 'specialUse', 'name', 'listed', 'subscribed', 'delimiter'].includes(key)) {
298
+ mailbox[key] = entry[key];
299
+ }
300
+ });
301
+ listingObject[normalizePath(entry.path)] = msgpack.encode(mailbox);
302
+ });
303
+
304
+ await this.redis.multi().del(this.getMailboxListKey()).hmset(this.getMailboxListKey(), listingObject).exec();
305
+ }
306
+
307
+ return listing;
308
+ }
309
+
310
+ async asyncPeriodicListRefresh() {
311
+ let accountData = await this.accountObject.loadAccountData();
312
+ let listing = await this.getCurrentListing();
313
+
314
+ let syncNeeded = new Set();
315
+ for (let entry of listing) {
316
+ if (
317
+ // previously unseen
318
+ !this.mailboxes.has(normalizePath(entry.path))
319
+ ) {
320
+ if (accountData.path && accountData.path !== '*') {
321
+ if (accountData.path !== entry.path) {
322
+ // ignore changes
323
+ entry.syncDisabled = true;
324
+ }
325
+ } else if (this.isGmail && !['\\All', '\\Junk', '\\Trash'].includes(entry.specialUse)) {
326
+ // do not look for changes from this folder
327
+ entry.syncDisabled = true;
328
+ }
329
+
330
+ let mailbox = new Mailbox(this, entry);
331
+ this.mailboxes.set(normalizePath(entry.path), mailbox);
332
+ syncNeeded.add(mailbox);
333
+ }
334
+ }
335
+
336
+ // sync new mailboxes
337
+ for (let mailbox of syncNeeded) {
338
+ await mailbox.sync();
339
+ }
340
+ }
341
+
342
+ periodicListRefresh() {
343
+ if (this.closing || this.closed) {
344
+ return false;
345
+ }
346
+ clearTimeout(this.refreshListingTimer);
347
+
348
+ this.refreshListingTimer = setTimeout(() => {
349
+ this.asyncPeriodicListRefresh()
350
+ .catch(err => {
351
+ this.logger.error({ msg: 'List refresh error', err });
352
+ })
353
+ .finally(() => {
354
+ this.periodicListRefresh();
355
+ });
356
+ }, LIST_REFRESH_DELAY);
357
+ this.refreshListingTimer.unref();
358
+ }
359
+
360
+ async connect() {
361
+ if (this.closing || this.closed) {
362
+ return false;
363
+ }
364
+
365
+ let imapClient = this.imapClient;
366
+
367
+ let accountData = await this.accountObject.loadAccountData();
368
+
369
+ // throws if connection fails
370
+ let response = await imapClient.connect();
371
+
372
+ let listing = await this.getCurrentListing();
373
+ this.periodicListRefresh();
374
+
375
+ // User might have disabled All Mail folder access and in that case we should treat it as a regular mailbox
376
+ this.isGmail = imapClient.capabilities.has('X-GM-EXT-1') && listing.some(entry => entry.specialUse === '\\All');
377
+
378
+ for (let entry of listing) {
379
+ if (accountData.path && accountData.path !== '*') {
380
+ if (accountData.path !== entry.path) {
381
+ entry.syncDisabled = true;
382
+ } else {
383
+ this.main = entry;
384
+ }
385
+ } else {
386
+ if ((this.isGmail && entry.specialUse === '\\All') || (!this.isGmail && entry.specialUse === '\\Inbox')) {
387
+ // In case of gmail prefer All mail folder as the folder to actively track, otherwise INBOX
388
+ // idle in this folder
389
+ this.main = entry;
390
+ }
391
+
392
+ if (this.isGmail && !['\\All', '\\Junk', '\\Trash'].includes(entry.specialUse)) {
393
+ // do not look for changes from this folder
394
+ entry.syncDisabled = true;
395
+ }
396
+ }
397
+
398
+ let mailbox = new Mailbox(this, entry);
399
+ this.mailboxes.set(normalizePath(entry.path), mailbox);
400
+ }
401
+
402
+ imapClient.on('expunge', event => {
403
+ if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
404
+ return; //?
405
+ }
406
+
407
+ let mailbox = this.mailboxes.get(normalizePath(event.path));
408
+ mailbox.onExpunge(event).catch(err => {
409
+ this.logger.error({ msg: 'Expunge error', err });
410
+ });
411
+ });
412
+
413
+ // Process untagged EXISTS responses
414
+ imapClient.on('exists', event => {
415
+ if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
416
+ return; //?
417
+ }
418
+
419
+ let mailbox = this.mailboxes.get(normalizePath(event.path));
420
+ mailbox.onExists(event).catch(err => {
421
+ this.logger.error({ msg: 'Exists error', err });
422
+ });
423
+ });
424
+
425
+ imapClient.on('mailboxOpen', event => {
426
+ if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
427
+ return; //?
428
+ }
429
+
430
+ let mailbox = this.mailboxes.get(normalizePath(event.path));
431
+ mailbox.onOpen(event).catch(err => {
432
+ this.logger.error({ msg: 'Open error', err });
433
+ });
434
+ });
435
+
436
+ imapClient.on('mailboxClose', event => {
437
+ if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
438
+ return; //?
439
+ }
440
+
441
+ let mailbox = this.mailboxes.get(normalizePath(event.path));
442
+ mailbox.onClose(event).catch(err => {
443
+ this.logger.error({ msg: 'Close error', err });
444
+ });
445
+ });
446
+
447
+ imapClient.on('flags', event => {
448
+ if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
449
+ return; //?
450
+ }
451
+
452
+ let mailbox = this.mailboxes.get(normalizePath(event.path));
453
+ mailbox.onFlags(event).catch(err => {
454
+ this.logger.error({ msg: 'Flags error', err });
455
+ });
456
+ });
457
+
458
+ imapClient.on('close', () => {
459
+ let handler = async () => {
460
+ for (let mailbox of this.mailboxes) {
461
+ if (mailbox.selected) {
462
+ // should be at most one though
463
+ await mailbox.onClose();
464
+ }
465
+ }
466
+
467
+ if (!imapClient.disabled) {
468
+ await this.reconnect();
469
+ }
470
+ };
471
+
472
+ this.logger.info({ msg: 'Connection closed', account: this.account });
473
+ handler().catch(err => {
474
+ this.logger.error({ msg: 'Connection close error', err });
475
+ });
476
+ });
477
+
478
+ return response;
479
+ }
480
+
481
+ async reconnect(force) {
482
+ if (this._connecting || this.closing || (this.closed && !force)) {
483
+ return false;
484
+ }
485
+
486
+ this._connecting = true;
487
+ this.closed = false;
488
+
489
+ try {
490
+ await backOff(() => this.start(), {
491
+ maxDelay: MAX_BACKOFF_DELAY,
492
+ numOfAttempts: Infinity,
493
+ retry: () => !this.closing && !this.closed,
494
+ startingDelay: 2000
495
+ });
496
+ } finally {
497
+ this._connecting = false;
498
+ }
499
+
500
+ try {
501
+ await this.checkIMAPConnection();
502
+ await this.syncMailboxes();
503
+ } catch (err) {
504
+ // ended in an unconncted state
505
+ this.logger.error({ msg: 'Failed to set up connection', err });
506
+ }
507
+ }
508
+
509
+ async syncMailboxes() {
510
+ clearTimeout(this.untaggedExpungeTimer);
511
+ clearTimeout(this.resyncTimer);
512
+ clearTimeout(this.completedTimer);
513
+
514
+ for (let mailbox of this.mailboxes.values()) {
515
+ await mailbox.sync();
516
+ }
517
+
518
+ this.state = 'connected';
519
+ await this.redis.hset(this.getAccountKey(), 'state', 'connected');
520
+ await this.redis.hset(this.getAccountKey(), 'lastErrorState', '{}');
521
+
522
+ let mainPath = this.main ? this.main.path : 'INBOX';
523
+ if (this.mailbox && normalizePath(this.mailbox.path) === normalizePath(mainPath)) {
524
+ // already selected
525
+ return;
526
+ }
527
+
528
+ // start waiting for changes
529
+ await this.select(mainPath);
530
+
531
+ // schedule next sync
532
+ this.resyncTimer = setTimeout(() => {
533
+ this.syncMailboxes().catch(err => {
534
+ this.logger.error({ msg: 'Mailbox Sync Error', err });
535
+ });
536
+ }, this.resyncDelay);
537
+ }
538
+
539
+ async select(path) {
540
+ if (!this.mailboxes.has(normalizePath(path))) {
541
+ // nothing to do here, mailbox not found
542
+ return;
543
+ }
544
+
545
+ let mailbox = this.mailboxes.get(normalizePath(path));
546
+ await mailbox.select();
547
+ }
548
+
549
+ getRandomId() {
550
+ let rid = BigInt('0x' + crypto.randomBytes(13).toString('hex')).toString(36);
551
+ if (rid.length < 20) {
552
+ rid = '0'.repeat(20 - rid.length) + rid;
553
+ } else if (rid.length > 20) {
554
+ rid = rid.substr(0, 20);
555
+ }
556
+ return rid;
557
+ }
558
+
559
+ async notify(mailbox, event, data) {
560
+ metrics(this.logger, 'events', 'inc', {
561
+ event
562
+ });
563
+
564
+ switch (event) {
565
+ case 'connectError':
566
+ case 'authenticationError':
567
+ return await this.setErrorState(event, data);
568
+ }
569
+
570
+ let payload = {
571
+ account: this.account,
572
+ date: new Date().toISOString()
573
+ };
574
+
575
+ let path = (mailbox && mailbox.path) || (data && data.path);
576
+ if (path) {
577
+ payload.path = path;
578
+ }
579
+
580
+ if (mailbox && mailbox.listingEntry && mailbox.listingEntry.specialUse) {
581
+ payload.specialUse = mailbox.listingEntry.specialUse;
582
+ }
583
+
584
+ if (event) {
585
+ payload.event = event;
586
+ }
587
+
588
+ if (data) {
589
+ payload.data = data;
590
+ }
591
+
592
+ await this.notifyQueue.add(event, payload, {
593
+ removeOnComplete: true,
594
+ removeOnFail: true,
595
+ attempts: 5,
596
+ backoff: {
597
+ type: 'exponential',
598
+ delay: 2000
599
+ }
600
+ });
601
+ }
602
+
603
+ getLocalAddress() {
604
+ // use default
605
+ if (!this.localAddresses.length) {
606
+ return { ip: false, name: false };
607
+ }
608
+ // just one
609
+ if (this.localAddresses.length === 1) {
610
+ return this.localAddresses[0];
611
+ }
612
+ // return random from list
613
+ return this.localAddresses[Math.floor(Math.random() * this.localAddresses.length)];
614
+ }
615
+
616
+ async start() {
617
+ let initialState = this.state;
618
+ if (this.imapClient) {
619
+ this.imapClient.disabled = true;
620
+ try {
621
+ this.imapClient.close();
622
+ } catch (err) {
623
+ this.logger.error({ msg: 'IMAP close error', err });
624
+ } finally {
625
+ this.imapClient = null;
626
+ }
627
+ }
628
+
629
+ try {
630
+ let accountData = await this.accountObject.loadAccountData();
631
+
632
+ this.notifyFrom = accountData.notifyFrom;
633
+
634
+ if (!accountData.imap && !accountData.oauth2) {
635
+ // can not make connection
636
+ this.state = 'unset';
637
+ return;
638
+ }
639
+
640
+ let imapConnectionConfig;
641
+ if (!accountData.imap && accountData.oauth2 && accountData.oauth2.auth) {
642
+ // load OAuth2 tokens
643
+ let now = Date.now();
644
+ let accessToken;
645
+ if (accountData.oauth2.expires < new Date(now + 30 * 1000)) {
646
+ // renew access token
647
+ try {
648
+ let oauthKeys = {
649
+ clientId: await settings.get('gmailClientId'),
650
+ clientSecret: await settings.get('gmailClientSecret'),
651
+ redirectUrl: await settings.get('gmailRedirectUrl')
652
+ };
653
+
654
+ if (!oauthKeys.clientId || !oauthKeys.clientSecret || !oauthKeys.redirectUrl) {
655
+ throw new Error('OAuth2 credentials not set up');
656
+ }
657
+
658
+ accountData = await this.accountObject.renewAccessToken(oauthKeys);
659
+ accessToken = accountData.oauth2.accessToken;
660
+ } catch (err) {
661
+ err.authenticationFailed = true;
662
+ await this.notify(false, AUTH_ERROR_NOTIFY, {
663
+ response: err.message,
664
+ serverResponseCode: 'OauthRenewError'
665
+ });
666
+ this.logger.error({
667
+ account: this.account,
668
+ err
669
+ });
670
+ this.state = AUTH_ERROR_NOTIFY;
671
+ throw err;
672
+ }
673
+ } else {
674
+ accessToken = accountData.oauth2.accessToken;
675
+ }
676
+
677
+ imapConnectionConfig = {
678
+ auth: {
679
+ user: accountData.oauth2.auth.user,
680
+ accessToken
681
+ },
682
+
683
+ host: 'imap.gmail.com',
684
+ port: 993,
685
+ secure: true,
686
+ resyncDelay: 900
687
+ };
688
+ } else {
689
+ // deep copy of imap settings
690
+ imapConnectionConfig = JSON.parse(JSON.stringify(accountData.imap));
691
+ }
692
+
693
+ // If authentication server is set then it overrides authentication data
694
+ if (imapConnectionConfig.useAuthServer) {
695
+ try {
696
+ imapConnectionConfig.auth = await resolveCredentials(this.account, 'imap');
697
+ } catch (err) {
698
+ err.authenticationFailed = true;
699
+ await this.notify(false, AUTH_ERROR_NOTIFY, {
700
+ response: err.message,
701
+ serverResponseCode: 'HTTPRequestError'
702
+ });
703
+ this.logger.error({
704
+ account: this.account,
705
+ err
706
+ });
707
+ this.state = AUTH_ERROR_NOTIFY;
708
+ throw err;
709
+ }
710
+ }
711
+
712
+ if (!imapConnectionConfig.tls) {
713
+ imapConnectionConfig.tls = {};
714
+ }
715
+ imapConnectionConfig.tls.localAddress = this.getLocalAddress().address;
716
+
717
+ let imapConfig = Object.assign(
718
+ {
719
+ resyncDelay: RESYNC_DELAY
720
+ },
721
+ imapConnectionConfig,
722
+ this.imapConfig
723
+ );
724
+ this.resyncDelay = imapConfig.resyncDelay * 1000;
725
+
726
+ this.imapClient = new ImapFlow(imapConfig);
727
+
728
+ // if emitLogs option is true then separate log event is fired for every log entry
729
+ this.imapClient.on('log', entry => {
730
+ if (!entry) {
731
+ return false;
732
+ }
733
+
734
+ if (typeof entry === 'string') {
735
+ // should not happen
736
+ entry = { msg: entry };
737
+ }
738
+
739
+ this.accountLogger.log(entry);
740
+ });
741
+
742
+ this.imapClient.on('error', err => {
743
+ this.logger.error({ msg: 'IMAP connection error', account: this.account, err });
744
+ this.reconnect().catch(err => {
745
+ this.logger.error({ msg: 'IMAP reconnection error', account: this.account, err });
746
+ });
747
+ });
748
+
749
+ try {
750
+ await this.connect();
751
+ await this.notify(false, AUTH_SUCCESS_NOTIFY, {
752
+ user: imapConfig.auth.user
753
+ });
754
+ } catch (err) {
755
+ if (err.authenticationFailed) {
756
+ this.logger.error({ msg: 'Failed to authenticate', account: this.account, err });
757
+ await this.notify(false, AUTH_ERROR_NOTIFY, {
758
+ response: err.response,
759
+ serverResponseCode: err.serverResponseCode
760
+ });
761
+ this.state = 'authenticationError';
762
+ } else {
763
+ this.logger.error({ msg: 'Failed to connect', account: this.account, err });
764
+ await this.notify(false, CONNECT_ERROR_NOTIFY, {
765
+ response: err.response || err.message,
766
+ serverResponseCode: err.serverResponseCode || err.code
767
+ });
768
+ this.state = 'connectError';
769
+ }
770
+ throw err;
771
+ }
772
+ } finally {
773
+ if (this.state !== initialState) {
774
+ // update state
775
+ try {
776
+ await this.redis.hset(this.getAccountKey(), 'state', this.state);
777
+ } catch (err) {
778
+ // ignore
779
+ }
780
+ }
781
+ }
782
+ }
783
+
784
+ async init() {
785
+ await this.reconnect();
786
+ }
787
+
788
+ async setErrorState(event, data) {
789
+ await this.redis.hmset(this.getAccountKey(), {
790
+ state: event,
791
+ lastErrorState: JSON.stringify(data)
792
+ });
793
+ }
794
+
795
+ async delete() {
796
+ if (this.closed || this.closing) {
797
+ return;
798
+ }
799
+ this.closing = true;
800
+
801
+ if (this.imapClient) {
802
+ this.imapClient.close();
803
+ }
804
+
805
+ clearTimeout(this.refreshListingTimer);
806
+ clearTimeout(this.untaggedExpungeTimer);
807
+ clearTimeout(this.resyncTimer);
808
+ clearTimeout(this.completedTimer);
809
+
810
+ try {
811
+ for (let [, mailbox] of this.mailboxes) {
812
+ await mailbox.clear({ skipNotify: true });
813
+ }
814
+
815
+ await this.redis.del(this.getMailboxListKey());
816
+ } finally {
817
+ this.closing = false;
818
+ this.closed = true;
819
+ }
820
+
821
+ this.logger.info({ msg: 'Closed account', account: this.account });
822
+ }
823
+
824
+ async close() {
825
+ this.state = 'disconnected';
826
+ if (this.closed || this.closing) {
827
+ return;
828
+ }
829
+ this.closing = true;
830
+
831
+ if (this.imapClient) {
832
+ this.imapClient.close();
833
+ }
834
+
835
+ clearTimeout(this.refreshListingTimer);
836
+ clearTimeout(this.untaggedExpungeTimer);
837
+ clearTimeout(this.resyncTimer);
838
+ clearTimeout(this.completedTimer);
839
+
840
+ this.closing = false;
841
+ this.closed = true;
842
+ }
843
+
844
+ isConnected() {
845
+ return this.imapClient && this.imapClient.usable && !this.closing && !this.closed;
846
+ }
847
+
848
+ currentState() {
849
+ if (this.state === 'connected' && !this.isConnected()) {
850
+ this.state = 'disconnected';
851
+ }
852
+ return this.state;
853
+ }
854
+
855
+ checkIMAPConnection() {
856
+ if (!this.isConnected()) {
857
+ let err = new Error('IMAP connection temporarily not available');
858
+ err.code = 'IMAPUnavailable';
859
+ err.statusCode = 503;
860
+ throw err;
861
+ }
862
+ }
863
+
864
+ // Mailbox level user methods
865
+
866
+ /**
867
+ * Fetch message text from IMAP. Resulting value is a unicode string.
868
+ *
869
+ * @param {string} textId ID of the text content
870
+ * @param {object} [options] Options object
871
+ * @param {number} [options.maxLength] If set then limits output stream to specified chars (NB! not bytes but unicode characters). Limit applies to each text type separately, so 1000 would mean you'd get a 1000 char string for plaintext and 1000 char string for html.
872
+ * @param {string} [options.contentType] If set then limits output for selected type only
873
+ * @returns {Object} Text object, where key is text type (either 'plain' or 'html') and value is a unicode string
874
+ */
875
+ async getText(textId, options) {
876
+ options = options || {};
877
+ this.checkIMAPConnection();
878
+
879
+ let { message, textParts } = await this.getMessageTextPaths(textId);
880
+ if (!message || !textParts || !textParts.length) {
881
+ return false;
882
+ }
883
+
884
+ if (!this.mailboxes.has(normalizePath(message.path))) {
885
+ return; //?
886
+ }
887
+
888
+ let mailbox = this.mailboxes.get(normalizePath(message.path));
889
+
890
+ let textType = (options.textType || '').toLowerCase().trim();
891
+
892
+ if (Array.isArray(textParts)) {
893
+ let re = /^\d+(\.\d+)*$/;
894
+ switch (textType) {
895
+ case 'plain':
896
+ textParts = Array.isArray(textParts[0]) ? textParts[0].filter(entry => re.test(entry)) : false;
897
+ break;
898
+ case 'html':
899
+ textParts = Array.isArray(textParts[1]) ? textParts[1].filter(entry => re.test(entry)) : false;
900
+ break;
901
+ default:
902
+ textParts = textParts.flatMap(part => part).filter(entry => re.test(entry));
903
+ break;
904
+ }
905
+ } else {
906
+ textParts = [];
907
+ }
908
+
909
+ let result = await mailbox.getText(message, textParts, options);
910
+
911
+ if (textType && textType !== '*') {
912
+ result = {
913
+ [textType]: result[textType] || '',
914
+ hasMore: result.hasMore
915
+ };
916
+ }
917
+
918
+ return result;
919
+ }
920
+
921
+ async getMessage(id, options) {
922
+ options = options || {};
923
+ this.checkIMAPConnection();
924
+
925
+ let buf = Buffer.from(id.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
926
+ let message = await this.unpackUid(buf.slice(0, 8));
927
+ if (!message) {
928
+ return false;
929
+ }
930
+
931
+ if (!this.mailboxes.has(normalizePath(message.path))) {
932
+ return false; //?
933
+ }
934
+
935
+ let mailbox = this.mailboxes.get(normalizePath(message.path));
936
+
937
+ return await mailbox.getMessage(message, options);
938
+ }
939
+
940
+ async updateMessage(id, updates) {
941
+ updates = updates || {};
942
+ this.checkIMAPConnection();
943
+
944
+ let buf = Buffer.from(id.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
945
+ let message = await this.unpackUid(buf.slice(0, 8));
946
+ if (!message) {
947
+ return false;
948
+ }
949
+
950
+ if (!this.mailboxes.has(normalizePath(message.path))) {
951
+ return false; //?
952
+ }
953
+
954
+ let mailbox = this.mailboxes.get(normalizePath(message.path));
955
+
956
+ return await mailbox.updateMessage(message, updates);
957
+ }
958
+
959
+ async moveMessage(id, target) {
960
+ target = target || {};
961
+
962
+ this.checkIMAPConnection();
963
+
964
+ let buf = Buffer.from(id.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
965
+ let message = await this.unpackUid(buf.slice(0, 8));
966
+
967
+ if (!message) {
968
+ return false;
969
+ }
970
+
971
+ if (!this.mailboxes.has(normalizePath(message.path))) {
972
+ return false; //?
973
+ }
974
+
975
+ let mailbox = this.mailboxes.get(normalizePath(message.path));
976
+ return await mailbox.moveMessage(message, target);
977
+ }
978
+
979
+ async deleteMessage(id) {
980
+ this.checkIMAPConnection();
981
+
982
+ let buf = Buffer.from(id.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
983
+ let message = await this.unpackUid(buf.slice(0, 8));
984
+ if (!message) {
985
+ return false;
986
+ }
987
+
988
+ if (!this.mailboxes.has(normalizePath(message.path))) {
989
+ return false; //?
990
+ }
991
+
992
+ let mailbox = this.mailboxes.get(normalizePath(message.path));
993
+
994
+ return await mailbox.deleteMessage(message);
995
+ }
996
+
997
+ /**
998
+ * Downloads an attachment from IMAP as a binary stream
999
+ *
1000
+ * @param {string} attachmentId ID of the attachment
1001
+ * @param {object} [options] Options object
1002
+ * @param {number} [options.maxLength] If set then limits output stream to specified bytes
1003
+ * @returns {Boolean|Stream} Attahcment stream or `false` if not found
1004
+ */
1005
+ async getAttachment(attachmentId, options) {
1006
+ this.checkIMAPConnection();
1007
+
1008
+ let buf = Buffer.from(attachmentId.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
1009
+ let id = buf.slice(0, 8);
1010
+ let part = buf.slice(8).toString();
1011
+
1012
+ let message = await this.unpackUid(id);
1013
+ if (!message) {
1014
+ return false;
1015
+ }
1016
+
1017
+ if (!this.mailboxes.has(normalizePath(message.path))) {
1018
+ return false; //?
1019
+ }
1020
+
1021
+ let mailbox = this.mailboxes.get(normalizePath(message.path));
1022
+
1023
+ return mailbox.getAttachment(message, part, options);
1024
+ }
1025
+
1026
+ /**
1027
+ * Downloads raw message from IMAP as a binary stream
1028
+ *
1029
+ * @param {string} id ID of the message
1030
+ * @param {object} [options] Options object
1031
+ * @param {number} [options.maxLength] If set then limits output stream to specified bytes
1032
+ * @returns {Boolean|Stream} Attahcment stream or `false` if not found
1033
+ */
1034
+ async getRawMessage(id, options) {
1035
+ this.checkIMAPConnection();
1036
+
1037
+ let buf = Buffer.from(id.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
1038
+ let message = await this.unpackUid(buf.slice(0, 8));
1039
+ if (!message) {
1040
+ return false;
1041
+ }
1042
+
1043
+ if (!this.mailboxes.has(normalizePath(message.path))) {
1044
+ return false; //?
1045
+ }
1046
+
1047
+ let mailbox = this.mailboxes.get(normalizePath(message.path));
1048
+
1049
+ return mailbox.getAttachment(message, false, options);
1050
+ }
1051
+
1052
+ async listMessages(options) {
1053
+ options = options || {};
1054
+ this.checkIMAPConnection();
1055
+
1056
+ if (!this.mailboxes.has(normalizePath(options.path))) {
1057
+ return false; //?
1058
+ }
1059
+
1060
+ let mailbox = this.mailboxes.get(normalizePath(options.path));
1061
+
1062
+ return mailbox.listMessages(options);
1063
+ }
1064
+
1065
+ async buildContacts() {
1066
+ this.checkIMAPConnection();
1067
+
1068
+ let addresses = [];
1069
+ let addressesMap = new Map();
1070
+
1071
+ for (let [, mailbox] of this.mailboxes) {
1072
+ if ((this.isGmail && mailbox.listingEntry.specialUse !== '\\All') || ['\\Junk', '\\Trash'].includes(mailbox.listingEntry.specialUse)) {
1073
+ // Only look into All Mail for Gmail account, ignore Junk/Trash for other account
1074
+ continue;
1075
+ }
1076
+
1077
+ let result = await mailbox.buildContacts();
1078
+ for (let address of result) {
1079
+ if (!address.address) {
1080
+ continue;
1081
+ }
1082
+
1083
+ address.address = address.address.replace(/\+[^@]*@/, '@');
1084
+
1085
+ if (!addressesMap.has(address.address)) {
1086
+ addressesMap.set(
1087
+ address.address,
1088
+ new Map([
1089
+ ['count', 0],
1090
+ ['names', new Map()],
1091
+ [
1092
+ 'types',
1093
+ new Map([
1094
+ ['from', 0],
1095
+ ['to', 0],
1096
+ ['cc', 0]
1097
+ ])
1098
+ ]
1099
+ ])
1100
+ );
1101
+ }
1102
+
1103
+ let addressMap = addressesMap.get(address.address);
1104
+
1105
+ addressMap.set('count', addressMap.get('count') + 1);
1106
+
1107
+ let typeMap = addressMap.get('types');
1108
+ if (typeMap.has(address.type)) {
1109
+ typeMap.set(address.type, typeMap.get(address.type) + 1);
1110
+ } else {
1111
+ typeMap.set(address.type, 1);
1112
+ }
1113
+
1114
+ if (address.name) {
1115
+ let nameMap = addressMap.get('names');
1116
+ if (nameMap.has(address.name)) {
1117
+ nameMap.set(address.name, nameMap.get(address.name) + 1);
1118
+ } else {
1119
+ nameMap.set(address.name, 1);
1120
+ }
1121
+ }
1122
+ }
1123
+ }
1124
+
1125
+ for (let [address, data] of addressesMap) {
1126
+ let names = data.has('names') ? Object.fromEntries(data.get('names').entries()) : {};
1127
+ let mainName = { name: '', count: -1 };
1128
+
1129
+ if (data.has('names')) {
1130
+ for (let [name, count] of data.get('names')) {
1131
+ if (name && count > mainName.count) {
1132
+ mainName = {
1133
+ name,
1134
+ count
1135
+ };
1136
+ }
1137
+ }
1138
+ }
1139
+
1140
+ addresses.push({
1141
+ address,
1142
+ name: mainName.name,
1143
+ count: data.get('count'),
1144
+ names,
1145
+ types: data.has('types') ? Object.fromEntries(data.get('types').entries()) : {}
1146
+ });
1147
+ }
1148
+
1149
+ addresses.sort((a, b) => b.count - a.count);
1150
+ return { addresses };
1151
+ }
1152
+
1153
+ async deleteMailbox(path) {
1154
+ this.checkIMAPConnection();
1155
+
1156
+ let result = {
1157
+ path,
1158
+ deleted: false // set to true if mailbox is actually deleted
1159
+ };
1160
+ try {
1161
+ let lock = await this.imapClient.getMailboxLock(path);
1162
+
1163
+ try {
1164
+ await this.imapClient.mailboxClose();
1165
+ try {
1166
+ await this.imapClient.mailboxDelete(path);
1167
+ result.deleted = true;
1168
+ } catch (err) {
1169
+ // kind of ignore
1170
+ }
1171
+ } finally {
1172
+ lock.release();
1173
+ }
1174
+ } catch (err) {
1175
+ this.logger.debug({ msg: 'Mailbox select error', path, err });
1176
+ }
1177
+
1178
+ if (this.mailboxes.has(normalizePath(path))) {
1179
+ let mailbox = this.mailboxes.get(normalizePath(path));
1180
+ await mailbox.clear();
1181
+ }
1182
+
1183
+ return result;
1184
+ }
1185
+
1186
+ async createMailbox(path) {
1187
+ this.checkIMAPConnection();
1188
+ let result = await this.imapClient.mailboxCreate(path);
1189
+ if (result) {
1190
+ result.created = !!result.created;
1191
+ }
1192
+
1193
+ let accountData = await this.accountObject.loadAccountData();
1194
+
1195
+ setImmediate(() => {
1196
+ this.getCurrentListing()
1197
+ .then(listing => {
1198
+ let syncNeeded = new Set();
1199
+ for (let entry of listing) {
1200
+ if (
1201
+ // previously unseen
1202
+ !this.mailboxes.has(normalizePath(entry.path))
1203
+ ) {
1204
+ if (accountData.path && accountData.path !== '*') {
1205
+ if (accountData.path !== entry.path) {
1206
+ // ignore changes
1207
+ entry.syncDisabled = true;
1208
+ }
1209
+ } else if (this.isGmail && !['\\All', '\\Junk', '\\Trash'].includes(entry.specialUse)) {
1210
+ // do not look for changes from this folder
1211
+ entry.syncDisabled = true;
1212
+ }
1213
+
1214
+ let mailbox = new Mailbox(this, entry);
1215
+ this.mailboxes.set(normalizePath(entry.path), mailbox);
1216
+ syncNeeded.add(mailbox);
1217
+ }
1218
+ }
1219
+
1220
+ let runSyncs = async () => {
1221
+ // sync new mailboxes
1222
+ for (let mailbox of syncNeeded) {
1223
+ await mailbox.sync();
1224
+ }
1225
+ };
1226
+
1227
+ return runSyncs();
1228
+ })
1229
+ .catch(err => {
1230
+ this.logger.error({ msg: 'List refresh error', err });
1231
+ });
1232
+ });
1233
+
1234
+ return result;
1235
+ }
1236
+
1237
+ async getSpecialUseMailbox(specialUse) {
1238
+ let storedListing = await this.redis.hgetallBuffer(this.getMailboxListKey());
1239
+ return Object.keys(storedListing || {})
1240
+ .map(path => {
1241
+ try {
1242
+ return msgpack.decode(storedListing[path]);
1243
+ } catch (err) {
1244
+ // should not happen
1245
+ }
1246
+ return false;
1247
+ })
1248
+ .filter(entry => entry)
1249
+ .find(entry => entry.specialUse === specialUse);
1250
+ }
1251
+
1252
+ async submitMessage(data) {
1253
+ let accountData = await this.accountObject.loadAccountData();
1254
+ if (!accountData.smtp && !accountData.oauth2) {
1255
+ // can not make connection
1256
+ let err = new Error('SMTP configuration not found');
1257
+ err.code = 'SMTPUnavailable';
1258
+ err.statusCode = 404;
1259
+ throw err;
1260
+ }
1261
+
1262
+ let smtpConnectionConfig;
1263
+ if (!accountData.smtp && accountData.oauth2 && accountData.oauth2.auth) {
1264
+ // load OAuth2 tokens
1265
+ let now = Date.now();
1266
+ let accessToken;
1267
+ if (accountData.oauth2.expires < now + 30 * 1000) {
1268
+ // renew access token
1269
+ try {
1270
+ let oauthKeys = {
1271
+ clientId: await settings.get('gmailClientId'),
1272
+ clientSecret: await settings.get('gmailClientSecret'),
1273
+ redirectUrl: await settings.get('gmailRedirectUrl')
1274
+ };
1275
+
1276
+ if (!oauthKeys.clientId || !oauthKeys.clientSecret || !oauthKeys.redirectUrl) {
1277
+ throw new Error('OAuth2 credentials not set up');
1278
+ }
1279
+
1280
+ accountData = await this.accountObject.renewAccessToken(oauthKeys);
1281
+ accessToken = accountData.oauth2.accessToken;
1282
+ } catch (err) {
1283
+ err.authenticationFailed = true;
1284
+ await this.notify(false, AUTH_ERROR_NOTIFY, {
1285
+ response: err.message,
1286
+ serverResponseCode: 'OauthRenewError'
1287
+ });
1288
+ this.logger.error({
1289
+ account: this.account,
1290
+ err
1291
+ });
1292
+ this.state = AUTH_ERROR_NOTIFY;
1293
+ throw err;
1294
+ }
1295
+ } else {
1296
+ accessToken = accountData.oauth2.accessToken;
1297
+ }
1298
+
1299
+ smtpConnectionConfig = {
1300
+ auth: {
1301
+ user: accountData.oauth2.auth.user,
1302
+ accessToken
1303
+ },
1304
+
1305
+ host: 'smtp.gmail.com',
1306
+ port: 465,
1307
+ secure: true,
1308
+ resyncDelay: 900
1309
+ };
1310
+ } else {
1311
+ // deep copy of imap settings
1312
+ smtpConnectionConfig = JSON.parse(JSON.stringify(accountData.smtp));
1313
+ }
1314
+
1315
+ let { raw, hasBcc, envelope, messageId, queueId, reference } = data;
1316
+
1317
+ let smtpAuth = smtpConnectionConfig.auth;
1318
+ // If authentication server is set then it overrides authentication data
1319
+ if (smtpConnectionConfig.useAuthServer) {
1320
+ try {
1321
+ smtpAuth = await resolveCredentials(this.account, 'smtp');
1322
+ } catch (err) {
1323
+ err.authenticationFailed = true;
1324
+ this.logger.error({
1325
+ account: this.account,
1326
+ err
1327
+ });
1328
+ throw err;
1329
+ }
1330
+ }
1331
+
1332
+ let { address, name } = this.getLocalAddress();
1333
+
1334
+ let smtpLogger = {};
1335
+ let smtpSettings = Object.assign(
1336
+ {
1337
+ name,
1338
+ localAddress: address,
1339
+ transactionLog: true,
1340
+ logger: smtpLogger
1341
+ },
1342
+ smtpConnectionConfig
1343
+ );
1344
+
1345
+ smtpSettings.auth = {
1346
+ user: smtpAuth.user
1347
+ };
1348
+
1349
+ if (smtpAuth.accessToken) {
1350
+ smtpSettings.auth.type = 'OAuth2';
1351
+ smtpSettings.auth.accessToken = smtpAuth.accessToken;
1352
+ } else {
1353
+ smtpSettings.auth.pass = smtpAuth.pass;
1354
+ }
1355
+
1356
+ for (let level of ['trace', 'debug', 'info', 'warn', 'error', 'fatal']) {
1357
+ smtpLogger[level] = (data, message, ...args) => {
1358
+ if (args && args.length) {
1359
+ message = util.format(message, ...args);
1360
+ }
1361
+ data.msg = message;
1362
+ data.sub = 'nodemailer';
1363
+ if (typeof this.logger[level] === 'function') {
1364
+ this.logger[level](data);
1365
+ } else {
1366
+ this.logger.debug(data);
1367
+ }
1368
+ };
1369
+ }
1370
+
1371
+ const transporter = nodemailer.createTransport(smtpSettings);
1372
+ try {
1373
+ const info = await transporter.sendMail({
1374
+ envelope,
1375
+ messageId,
1376
+ // make sure that Bcc line is removed from the version sent to SMTP
1377
+ raw: !hasBcc ? raw : await removeBcc(raw)
1378
+ });
1379
+
1380
+ if (
1381
+ !this.isGmail &&
1382
+ // if account settings do not have a copy value or it is set to true
1383
+ (!Object.prototype.hasOwnProperty.call(accountData, 'copy') || accountData.copy)
1384
+ ) {
1385
+ // Upload message to Sent Mail folder. Gmail does this automatically.
1386
+ try {
1387
+ this.checkIMAPConnection();
1388
+ let sentMailbox = await this.getSpecialUseMailbox('\\Sent');
1389
+ if (sentMailbox) {
1390
+ await this.imapClient.append(sentMailbox.path, raw, ['\\Seen']);
1391
+ }
1392
+ } catch (err) {
1393
+ this.logger.error({ msg: 'Failed to upload Sent mail', queueId, messageId, err });
1394
+ }
1395
+ }
1396
+
1397
+ // Add \Answered flag to referenced message if needed
1398
+ if (reference && reference.update) {
1399
+ try {
1400
+ this.checkIMAPConnection();
1401
+ await this.updateMessage(reference.message, {
1402
+ flags: {
1403
+ add: ['\\Answered']
1404
+ }
1405
+ });
1406
+ } catch (err) {
1407
+ this.logger.error({ msg: 'Failed to update reference flags', queueId, messageId, reference, err });
1408
+ }
1409
+ }
1410
+
1411
+ await this.notify(false, EMAIL_SENT_NOTIFY, {
1412
+ messageId: info.messageId,
1413
+ response: info.response,
1414
+ queueId,
1415
+ envelope
1416
+ });
1417
+
1418
+ return {
1419
+ response: info.response,
1420
+ messageId: info.messageId
1421
+ };
1422
+ } catch (err) {
1423
+ err.code = 'SubmitFail';
1424
+ err.statusCode = 502;
1425
+ throw err;
1426
+ }
1427
+ }
1428
+
1429
+ async queueMessage(data) {
1430
+ let accountData = await this.accountObject.loadAccountData();
1431
+ if (!accountData.smtp && !accountData.oauth2) {
1432
+ // can not make connection
1433
+ let err = new Error('SMTP configuration not found');
1434
+ err.code = 'SMTPUnavailable';
1435
+ err.statusCode = 404;
1436
+ throw err;
1437
+ }
1438
+
1439
+ // normal message
1440
+ data.disableFileAccess = true;
1441
+ data.disableUrlAccess = true;
1442
+
1443
+ // convert data uri images to attachments
1444
+ data.attachDataUrls = true;
1445
+
1446
+ // Resolve reference and update reference/in-reply-to headers
1447
+ if (data.reference && data.reference.message) {
1448
+ let referencedMessage;
1449
+ try {
1450
+ this.checkIMAPConnection();
1451
+ referencedMessage = await this.getMessage(data.reference.message, {
1452
+ fields: {
1453
+ uid: true,
1454
+ flags: true,
1455
+ envelope: true,
1456
+ headers: ['references']
1457
+ }
1458
+ });
1459
+ if (!referencedMessage) {
1460
+ throw new Error('Referenced message was not found');
1461
+ }
1462
+ } catch (err) {
1463
+ this.logger.error({ msg: 'Failed to get referenced message', reference: data.reference.message, err });
1464
+ }
1465
+
1466
+ if (referencedMessage) {
1467
+ let references = []
1468
+ .concat(referencedMessage.messageId || [])
1469
+ .concat(referencedMessage.inReplyTo || [])
1470
+ .concat((referencedMessage.headers && referencedMessage.headers.references) || [])
1471
+ .flatMap(line => line.split(/\s+/))
1472
+ .map(ref => ref.trim())
1473
+ .filter(ref => ref)
1474
+ .map(ref => {
1475
+ if (!/^</.test(ref)) {
1476
+ ref = '<' + ref;
1477
+ }
1478
+ if (!/>$/.test(ref)) {
1479
+ ref = ref + '>';
1480
+ }
1481
+ return ref;
1482
+ });
1483
+
1484
+ references = Array.from(new Set(references));
1485
+ if (references.length) {
1486
+ if (!data.headers) {
1487
+ data.headers = {};
1488
+ }
1489
+ data.headers.references = references.join(' ');
1490
+ }
1491
+
1492
+ if (data.reference.action === 'reply' && referencedMessage.messageId) {
1493
+ data.headers['in-reply-to'] = referencedMessage.messageId;
1494
+ }
1495
+
1496
+ if (!referencedMessage.flags || !referencedMessage.flags.includes('\\Answered')) {
1497
+ data.reference.update = true;
1498
+ }
1499
+
1500
+ if (!data.subject && referencedMessage.subject) {
1501
+ let subject = referencedMessage.subject;
1502
+ let prefix;
1503
+ switch (data.reference.action) {
1504
+ case 'reply':
1505
+ if (!/^Re:/i.test(subject)) {
1506
+ prefix = 'Re';
1507
+ }
1508
+ break;
1509
+ case 'forward':
1510
+ if (!/^Fwd:/i.test(subject)) {
1511
+ prefix = 'Fwd';
1512
+ }
1513
+ break;
1514
+ }
1515
+ data.subject = `${prefix ? prefix + ': ' : ''}${subject}`;
1516
+ }
1517
+ }
1518
+ }
1519
+
1520
+ const { raw, hasBcc, envelope, messageId, sendAt } = await getRawEmail(data);
1521
+
1522
+ let now = new Date();
1523
+
1524
+ //queue for later
1525
+ let qId = crypto.randomBytes(8).toString('hex');
1526
+ let msgEntry = msgpack.encode({
1527
+ qId,
1528
+ hasBcc,
1529
+ envelope,
1530
+ messageId,
1531
+ reference: data.reference || {},
1532
+ sendAt: (sendAt && sendAt.getTime()) || false,
1533
+ created: now.getTime(),
1534
+ raw
1535
+ });
1536
+
1537
+ await this.redis.hsetBuffer(`iaq:${this.account}`, qId, msgEntry);
1538
+
1539
+ let job;
1540
+ if (sendAt && sendAt > now) {
1541
+ job = await this.submitQueue.add(
1542
+ 'delayed',
1543
+ { account: this.account, qId, messageId, created: now.getTime() },
1544
+ {
1545
+ removeOnComplete: true,
1546
+ removeOnFail: true,
1547
+ attempts: 5,
1548
+ backoff: {
1549
+ type: 'exponential',
1550
+ delay: 2000
1551
+ },
1552
+ delay: sendAt.getTime() - now.getTime()
1553
+ }
1554
+ );
1555
+ } else {
1556
+ job = await this.submitQueue.add(
1557
+ 'queued',
1558
+ { account: this.account, qId, messageId, created: now.getTime() },
1559
+ {
1560
+ removeOnComplete: true,
1561
+ removeOnFail: true,
1562
+ attempts: 5,
1563
+ backoff: {
1564
+ type: 'exponential',
1565
+ delay: 2000
1566
+ }
1567
+ }
1568
+ );
1569
+ }
1570
+
1571
+ this.logger.info({ msg: 'Message queued for delivery', envelope, messageId, sendAt: (sendAt || now).toISOString(), queueId: qId, job: job.id });
1572
+
1573
+ return {
1574
+ response: 'Queued for delivery',
1575
+ messageId,
1576
+ sendAt: (sendAt || now).toISOString(),
1577
+ queueId: qId
1578
+ };
1579
+ }
1580
+
1581
+ async uploadMessage(data) {
1582
+ this.checkIMAPConnection();
1583
+
1584
+ let accountData = await this.accountObject.loadAccountData();
1585
+ if (!accountData.smtp) {
1586
+ // can not make connection
1587
+ let err = new Error('SMTP configuration not found');
1588
+ err.code = 'SMTPUnavailable';
1589
+ err.statusCode = 404;
1590
+ throw err;
1591
+ }
1592
+
1593
+ data.disableFileAccess = true;
1594
+ data.disableUrlAccess = true;
1595
+
1596
+ // convert data uri images to attachments
1597
+ data.attachDataUrls = true;
1598
+
1599
+ // Resolve reference and update reference/in-reply-to headers
1600
+ if (data.reference && data.reference.message) {
1601
+ let referencedMessage = await this.getMessage(data.reference.message, {
1602
+ fields: {
1603
+ uid: true,
1604
+ flags: true,
1605
+ envelope: true,
1606
+ headers: ['references']
1607
+ }
1608
+ });
1609
+
1610
+ if (!referencedMessage) {
1611
+ let err = new Error('Referenced message was not found');
1612
+ err.code = 'MessageNotFound';
1613
+ err.statusCode = 404;
1614
+ throw err;
1615
+ }
1616
+
1617
+ let references = []
1618
+ .concat(referencedMessage.messageId || [])
1619
+ .concat(referencedMessage.inReplyTo || [])
1620
+ .concat((referencedMessage.headers && referencedMessage.headers.references) || [])
1621
+ .flatMap(line => line.split(/\s+/))
1622
+ .map(ref => ref.trim())
1623
+ .filter(ref => ref)
1624
+ .map(ref => {
1625
+ if (!/^</.test(ref)) {
1626
+ ref = '<' + ref;
1627
+ }
1628
+ if (!/>$/.test(ref)) {
1629
+ ref = ref + '>';
1630
+ }
1631
+ return ref;
1632
+ });
1633
+
1634
+ references = Array.from(new Set(references));
1635
+ if (references.length) {
1636
+ if (!data.headers) {
1637
+ data.headers = {};
1638
+ }
1639
+ data.headers.references = references.join(' ');
1640
+ }
1641
+
1642
+ if (data.reference.action === 'reply' && referencedMessage.messageId) {
1643
+ data.headers['in-reply-to'] = referencedMessage.messageId;
1644
+ }
1645
+
1646
+ if (!referencedMessage.flags || !referencedMessage.flags.includes('\\Answered')) {
1647
+ try {
1648
+ await this.updateMessage(data.reference.message, {
1649
+ flags: {
1650
+ add: ['\\Answered']
1651
+ }
1652
+ });
1653
+ } catch (err) {
1654
+ this.logger.error(err);
1655
+ }
1656
+ }
1657
+
1658
+ if (!data.subject && referencedMessage.subject) {
1659
+ let subject = referencedMessage.subject;
1660
+ let prefix;
1661
+ switch (data.reference.action) {
1662
+ case 'reply':
1663
+ if (!/^Re:/i.test(subject)) {
1664
+ prefix = 'Re';
1665
+ }
1666
+ break;
1667
+ case 'forward':
1668
+ if (!/^Fwd:/i.test(subject)) {
1669
+ prefix = 'Fwd';
1670
+ }
1671
+ break;
1672
+ }
1673
+ data.subject = `${prefix ? prefix + ': ' : ''}${subject}`;
1674
+ }
1675
+ }
1676
+
1677
+ const mail = new MailComposer(data);
1678
+ let raw = await mail.compile().build();
1679
+
1680
+ // Upload message to selected folder
1681
+ try {
1682
+ let lock = await this.imapClient.getMailboxLock(data.path);
1683
+ let response = {};
1684
+
1685
+ try {
1686
+ let uploadResponse = await this.imapClient.append(data.path, raw, data.flags);
1687
+
1688
+ response.path = uploadResponse.path;
1689
+
1690
+ if (uploadResponse.uid) {
1691
+ response.uid = uploadResponse.uid;
1692
+ }
1693
+
1694
+ if (uploadResponse.uidValidity) {
1695
+ response.uidValidity = uploadResponse.uidValidity.toString();
1696
+ }
1697
+
1698
+ if (uploadResponse.seq) {
1699
+ response.seq = uploadResponse.seq;
1700
+ }
1701
+
1702
+ if (response.uid) {
1703
+ response.id = await this.packUid(response.path, response.uid);
1704
+ }
1705
+ } finally {
1706
+ lock.release();
1707
+ }
1708
+
1709
+ return response;
1710
+ } catch (err) {
1711
+ err.code = 'UploadFail';
1712
+ err.statusCode = 502;
1713
+ throw err;
1714
+ }
1715
+ }
1716
+
1717
+ getLogger() {
1718
+ this.mainLogger =
1719
+ this.options.logger ||
1720
+ logger.child({
1721
+ component: 'imap-client',
1722
+ account: this.account,
1723
+ cid: this.cid
1724
+ });
1725
+
1726
+ let synteticLogger = {};
1727
+ let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
1728
+ for (let level of levels) {
1729
+ synteticLogger[level] = (...args) => {
1730
+ this.mainLogger[level](...args);
1731
+
1732
+ if (this.emitLogs && args && args[0] && typeof args[0] === 'object') {
1733
+ let entry = Object.assign({ level, t: Date.now(), cid: this.cid }, args[0]);
1734
+ if (entry.err && typeof entry.err === 'object') {
1735
+ let err = entry.err;
1736
+ entry.err = {
1737
+ stack: err.stack
1738
+ };
1739
+ // enumerable error fields
1740
+ Object.keys(err).forEach(key => {
1741
+ entry.err[key] = err[key];
1742
+ });
1743
+ }
1744
+
1745
+ this.accountLogger.log(entry);
1746
+ }
1747
+ };
1748
+ }
1749
+ return synteticLogger;
1750
+ }
1751
+ }
1752
+
1753
+ module.exports.Connection = Connection;